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.Api.Host/Controllers/AnalyticsController.cs b/Polyclinic/Polyclinic.Api.Host/Controllers/AnalyticsController.cs
new file mode 100644
index 000000000..ee88fceb5
--- /dev/null
+++ b/Polyclinic/Polyclinic.Api.Host/Controllers/AnalyticsController.cs
@@ -0,0 +1,117 @@
+using Microsoft.AspNetCore.Mvc;
+using Polyclinic.Application.Contracts;
+using Polyclinic.Application.Contracts.Analytics;
+using Polyclinic.Application.Contracts.Appointments;
+using Polyclinic.Application.Contracts.Doctors;
+using Polyclinic.Application.Contracts.Patients;
+
+namespace Polyclinic.Api.Host.Controllers;
+
+///
+/// Контроллер для получения аналитических отчетов и выборок
+///
+[ApiController]
+[Route("api/[controller]")]
+public class AnalyticsController(IAnalyticsService analyticsService) : ControllerBase
+{
+ ///
+ /// Получить врачей со стажем работы более указанного (по умолчанию 10 лет)
+ ///
+ /// Минимальный стаж в годах
+ [HttpGet("doctors/experienced")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> GetExperiencedDoctors([FromQuery] int minExperience = 10)
+ {
+ try
+ {
+ var result = await analyticsService.GetDoctorsWithExperienceAsync(minExperience);
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
+ }
+ }
+
+ ///
+ /// Получить список пациентов, записанных к конкретному врачу, отсортированный по ФИО
+ ///
+ /// Идентификатор врача
+ [HttpGet("doctors/{doctorId}/patients")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> GetPatientsByDoctor(int doctorId)
+ {
+ try
+ {
+ var result = await analyticsService.GetPatientsByDoctorAsync(doctorId);
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
+ }
+ }
+
+ ///
+ /// Получить статистику повторных приемов за указанный месяц
+ ///
+ /// Дата для определения месяца и года
+ [HttpGet("appointments/stats/monthly")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task> GetMonthlyStats([FromQuery] DateTime date)
+ {
+ try
+ {
+ var result = await analyticsService.GetMonthlyRepeatStatsAsync(date);
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
+ }
+ }
+
+ ///
+ /// Получить пациентов старше указанного возраста (по умолчанию 30), посетивших более одного врача
+ ///
+ /// Минимальный возраст
+ [HttpGet("patients/active-visitors")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> GetPatientsWithMultipleDoctors([FromQuery] int minAge = 30)
+ {
+ try
+ {
+ var result = await analyticsService.GetPatientsWithMultipleDoctorsAsync(minAge);
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
+ }
+ }
+
+ ///
+ /// Получить список приемов в конкретном кабинете за месяц
+ ///
+ /// Номер кабинета
+ /// Дата для определения месяца выборки
+ [HttpGet("rooms/{roomNumber}/appointments")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> GetAppointmentsByRoom(string roomNumber, [FromQuery] DateTime date)
+ {
+ try
+ {
+ var result = await analyticsService.GetAppointmentsByRoomAsync(roomNumber, date);
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
+ }
+ }
+}
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Api.Host/Controllers/AppointmentController.cs b/Polyclinic/Polyclinic.Api.Host/Controllers/AppointmentController.cs
new file mode 100644
index 000000000..ab46376ee
--- /dev/null
+++ b/Polyclinic/Polyclinic.Api.Host/Controllers/AppointmentController.cs
@@ -0,0 +1,11 @@
+using Polyclinic.Application.Contracts;
+using Polyclinic.Application.Contracts.Appointments;
+
+namespace Polyclinic.Api.Host.Controllers;
+
+///
+/// Контроллер для управления записями на прием
+///
+public class AppointmentsController(
+ IApplicationService service)
+ : CrudControllerBase(service);
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Api.Host/Controllers/CrudControllerBase.cs b/Polyclinic/Polyclinic.Api.Host/Controllers/CrudControllerBase.cs
new file mode 100644
index 000000000..10f43ada2
--- /dev/null
+++ b/Polyclinic/Polyclinic.Api.Host/Controllers/CrudControllerBase.cs
@@ -0,0 +1,137 @@
+using Microsoft.AspNetCore.Mvc;
+using Polyclinic.Application.Contracts;
+
+namespace Polyclinic.Api.Host.Controllers;
+
+///
+/// Базовый абстрактный контроллер, реализующий стандартные CRUD-операции
+///
+/// Тип DTO для чтения
+/// Тип DTO для создания и обновления
+/// Тип идентификатора сущности
+[ApiController]
+[Route("api/[controller]")]
+public abstract class CrudControllerBase(
+ IApplicationService service)
+ : ControllerBase
+ where TDto : class
+ where TCreateUpdateDto : class
+ where TKey : struct
+{
+ ///
+ /// Получает список всех сущностей
+ ///
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public virtual async Task>> GetAll()
+ {
+ try
+ {
+ var result = await service.GetAll();
+ return Ok(result);
+ }
+ catch (Exception ex)
+ {
+ return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
+ }
+ }
+
+ ///
+ /// Получает сущность по идентификатору
+ ///
+ /// Идентификатор сущности
+ [HttpGet("{id}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public virtual async Task> Get(TKey id)
+ {
+ try
+ {
+ var result = await service.Get(id);
+ if (result == null) return NotFound();
+
+ return Ok(result);
+ }
+ catch (KeyNotFoundException ex)
+ {
+ return NotFound(new { error = ex.Message });
+ }
+ catch (Exception ex)
+ {
+ return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
+ }
+ }
+
+ ///
+ /// Создает новую сущность
+ ///
+ /// DTO с данными для создания
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public virtual async Task> Create([FromBody] TCreateUpdateDto dto)
+ {
+ try
+ {
+ var result = await service.Create(dto);
+ return CreatedAtAction(nameof(this.Create), result);
+ }
+ catch (KeyNotFoundException ex)
+ {
+ return NotFound(new { error = ex.Message });
+ }
+ catch (Exception ex)
+ {
+ return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
+ }
+ }
+
+ ///
+ /// Обновляет существующую сущность
+ ///
+ /// Идентификатор обновляемой сущности
+ /// DTO с новыми данными
+ [HttpPut("{id}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public virtual async Task> Update(TKey id, [FromBody] TCreateUpdateDto dto)
+ {
+ try
+ {
+ var result = await service.Update(dto, id);
+ return Ok(result);
+ }
+ catch (KeyNotFoundException ex)
+ {
+ return NotFound(new { error = ex.Message });
+ }
+ catch (Exception ex)
+ {
+ return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
+ }
+ }
+
+ ///
+ /// Удаляет сущность по идентификатору
+ ///
+ /// Идентификатор удаляемой сущности
+ [HttpDelete("{id}")]
+ [ProducesResponseType(StatusCodes.Status204NoContent)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public virtual async Task Delete(TKey id)
+ {
+ try
+ {
+ await service.Delete(id);
+ return NoContent();
+ }
+ catch (Exception ex)
+ {
+ return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
+ }
+ }
+}
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Api.Host/Controllers/DoctorController.cs b/Polyclinic/Polyclinic.Api.Host/Controllers/DoctorController.cs
new file mode 100644
index 000000000..9def16d00
--- /dev/null
+++ b/Polyclinic/Polyclinic.Api.Host/Controllers/DoctorController.cs
@@ -0,0 +1,11 @@
+using Polyclinic.Application.Contracts;
+using Polyclinic.Application.Contracts.Doctors;
+
+namespace Polyclinic.Api.Host.Controllers;
+
+///
+/// Контроллер для управления данными врачей
+///
+public class DoctorController(
+ IApplicationService service)
+ : CrudControllerBase(service);
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Api.Host/Controllers/PatientController.cs b/Polyclinic/Polyclinic.Api.Host/Controllers/PatientController.cs
new file mode 100644
index 000000000..0d01d2a3d
--- /dev/null
+++ b/Polyclinic/Polyclinic.Api.Host/Controllers/PatientController.cs
@@ -0,0 +1,11 @@
+using Polyclinic.Application.Contracts;
+using Polyclinic.Application.Contracts.Patients;
+
+namespace Polyclinic.Api.Host.Controllers;
+
+///
+/// Контроллер для управления данными пациентов
+///
+public class PatientsController(
+ IApplicationService service)
+ : CrudControllerBase(service);
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Api.Host/Controllers/SpecializationController.cs b/Polyclinic/Polyclinic.Api.Host/Controllers/SpecializationController.cs
new file mode 100644
index 000000000..3c3d95eb6
--- /dev/null
+++ b/Polyclinic/Polyclinic.Api.Host/Controllers/SpecializationController.cs
@@ -0,0 +1,11 @@
+using Polyclinic.Application.Contracts;
+using Polyclinic.Application.Contracts.Specializations;
+
+namespace Polyclinic.Api.Host.Controllers;
+
+///
+/// Контроллер для управления справочником специализаций
+///
+public class SpecializationController(
+ IApplicationService service)
+ : CrudControllerBase(service);
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Api.Host/Polyclinic.Api.Host.csproj b/Polyclinic/Polyclinic.Api.Host/Polyclinic.Api.Host.csproj
new file mode 100644
index 000000000..e8741d67d
--- /dev/null
+++ b/Polyclinic/Polyclinic.Api.Host/Polyclinic.Api.Host.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Polyclinic/Polyclinic.Api.Host/Program.cs b/Polyclinic/Polyclinic.Api.Host/Program.cs
new file mode 100644
index 000000000..c5058d856
--- /dev/null
+++ b/Polyclinic/Polyclinic.Api.Host/Program.cs
@@ -0,0 +1,89 @@
+using Microsoft.EntityFrameworkCore;
+using Polyclinic.Application;
+using Polyclinic.Application.Contracts;
+using Polyclinic.Application.Contracts.Appointments;
+using Polyclinic.Application.Contracts.Doctors;
+using Polyclinic.Application.Contracts.Patients;
+using Polyclinic.Application.Contracts.Specializations;
+using Polyclinic.Application.Services;
+using Polyclinic.Domain;
+using Polyclinic.Domain.Entities;
+using Polyclinic.Infrastructure.EfCore;
+using Polyclinic.Infrastructure.EfCore.Repositories;
+using Polyclinic.Infrastructure.RabbitMq;
+using Polyclinic.ServiceDefaults;
+using System.Text.Json.Serialization;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddAutoMapper(config =>
+{
+ config.AddProfile(new PolyclinicProfile());
+});
+
+builder.Services.AddSingleton();
+
+builder.AddServiceDefaults();
+
+builder.Services.AddTransient, SpecializationRepository>();
+builder.Services.AddTransient, PatientRepository>();
+builder.Services.AddTransient, DoctorRepository>();
+builder.Services.AddTransient, AppointmentRepository>();
+
+builder.Services.AddScoped();
+builder.Services.AddScoped, SpecializationService>();
+builder.Services.AddScoped, PatientService>();
+builder.Services.AddScoped, DoctorService>();
+builder.Services.AddScoped, AppointmentService>();
+
+builder.Services.AddControllers()
+ .AddJsonOptions(options =>
+ {
+ options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
+ }); builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(c =>
+{
+ var assemblies = AppDomain.CurrentDomain.GetAssemblies()
+ .Where(a => a.GetName().Name!.StartsWith("Polyclinic"))
+ .Distinct();
+
+ foreach (var assembly in assemblies)
+ {
+ var xmlFile = $"{assembly.GetName().Name}.xml";
+ var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
+ if (File.Exists(xmlPath))
+ c.IncludeXmlComments(xmlPath);
+ }
+
+ c.UseInlineDefinitionsForEnums();
+});
+
+builder.AddSqlServerDbContext("DatabaseConnectionString");
+
+builder.AddRabbitMQClient("rabbitMqConnection");
+
+builder.Services.Configure(builder.Configuration.GetSection("RabbitMq"));
+
+builder.Services.AddHostedService();
+
+var app = builder.Build();
+
+app.MapDefaultEndpoints();
+
+if (app.Environment.IsDevelopment())
+{
+ using var scope = app.Services.CreateScope();
+ var db = scope.ServiceProvider.GetRequiredService();
+ await db.Database.MigrateAsync();
+
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+
+app.UseAuthorization();
+
+app.MapControllers();
+
+app.Run();
diff --git a/Polyclinic/Polyclinic.Api.Host/Properties/launchSettings.json b/Polyclinic/Polyclinic.Api.Host/Properties/launchSettings.json
new file mode 100644
index 000000000..6c75e264a
--- /dev/null
+++ b/Polyclinic/Polyclinic.Api.Host/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:20801",
+ "sslPort": 44366
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5087",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7184;http://localhost:5087",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/Polyclinic/Polyclinic.Api.Host/appsettings.Development.json b/Polyclinic/Polyclinic.Api.Host/appsettings.Development.json
new file mode 100644
index 000000000..0c208ae91
--- /dev/null
+++ b/Polyclinic/Polyclinic.Api.Host/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/Polyclinic/Polyclinic.Api.Host/appsettings.json b/Polyclinic/Polyclinic.Api.Host/appsettings.json
new file mode 100644
index 000000000..222e0e158
--- /dev/null
+++ b/Polyclinic/Polyclinic.Api.Host/appsettings.json
@@ -0,0 +1,14 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "RabbitMq": {
+ "QueueName": "appointment-contracts",
+ "RetryCount": 5,
+ "RetryDelayMs": 1000
+ }
+}
diff --git a/Polyclinic/Polyclinic.AppHost/AppHost.cs b/Polyclinic/Polyclinic.AppHost/AppHost.cs
new file mode 100644
index 000000000..15693d94b
--- /dev/null
+++ b/Polyclinic/Polyclinic.AppHost/AppHost.cs
@@ -0,0 +1,19 @@
+var builder = DistributedApplication.CreateBuilder(args);
+
+var sqlDb = builder.AddSqlServer("polyclinic-sql-server")
+ .AddDatabase("PolyclinicDb");
+
+var rabbitmq = builder.AddRabbitMQ("rabbitMqConnection")
+ .WithManagementPlugin();
+
+builder.AddProject("polyclinic-api-host")
+ .WithReference(sqlDb, "DatabaseConnectionString")
+ .WithReference(rabbitmq)
+ .WaitFor(sqlDb)
+ .WaitFor(rabbitmq);
+
+builder.AddProject("polyclinic-generator")
+ .WithReference(rabbitmq)
+ .WaitFor(rabbitmq);
+
+builder.Build().Run();
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.AppHost/Polyclinic.AppHost.csproj b/Polyclinic/Polyclinic.AppHost/Polyclinic.AppHost.csproj
new file mode 100644
index 000000000..d2527de5c
--- /dev/null
+++ b/Polyclinic/Polyclinic.AppHost/Polyclinic.AppHost.csproj
@@ -0,0 +1,24 @@
+
+
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ 8caaa9c7-a496-41cb-a0fe-a05874e7d079
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Polyclinic/Polyclinic.AppHost/Properties/launchSettings.json b/Polyclinic/Polyclinic.AppHost/Properties/launchSettings.json
new file mode 100644
index 000000000..e321feca2
--- /dev/null
+++ b/Polyclinic/Polyclinic.AppHost/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:17108;http://localhost:15280",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21020",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22277"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15280",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19195",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20298"
+ }
+ }
+ }
+}
diff --git a/Polyclinic/Polyclinic.AppHost/appsettings.Development.json b/Polyclinic/Polyclinic.AppHost/appsettings.Development.json
new file mode 100644
index 000000000..0c208ae91
--- /dev/null
+++ b/Polyclinic/Polyclinic.AppHost/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/Polyclinic/Polyclinic.AppHost/appsettings.json b/Polyclinic/Polyclinic.AppHost/appsettings.json
new file mode 100644
index 000000000..31c092aa4
--- /dev/null
+++ b/Polyclinic/Polyclinic.AppHost/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ }
+}
diff --git a/Polyclinic/Polyclinic.Application.Contracts/Analytics/MonthlyAppointmentStatsDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Analytics/MonthlyAppointmentStatsDto.cs
new file mode 100644
index 000000000..0d0719394
--- /dev/null
+++ b/Polyclinic/Polyclinic.Application.Contracts/Analytics/MonthlyAppointmentStatsDto.cs
@@ -0,0 +1,15 @@
+namespace Polyclinic.Application.Contracts.Analytics;
+
+///
+/// DTO со статистикой приемов за месяц
+///
+/// Год статистики
+/// Месяц статистики
+/// Количество повторных приемов
+/// Общее количество приемов
+public record MonthlyAppointmentStatsDto(
+ int Year,
+ int Month,
+ int RepeatAppointmentCount,
+ int TotalAppointmentCount
+);
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Application.Contracts/Appointments/AppointmentCreateUpdateDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Appointments/AppointmentCreateUpdateDto.cs
new file mode 100644
index 000000000..cca75b46a
--- /dev/null
+++ b/Polyclinic/Polyclinic.Application.Contracts/Appointments/AppointmentCreateUpdateDto.cs
@@ -0,0 +1,30 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Polyclinic.Application.Contracts.Appointments;
+
+///
+/// DTO для создания и обновления записи на прием
+///
+/// Дата и время приема
+/// Номер кабинета
+/// Признак повторного приема
+/// Идентификатор пациента
+/// Идентификатор врача
+public record AppointmentCreateUpdateDto(
+ [Required(ErrorMessage = "Дата и время приема обязательны")]
+ DateTime AppointmentDateTime,
+
+ [Required(ErrorMessage = "Номер кабинета обязателен")]
+ [MaxLength(10, ErrorMessage = "Номер кабинета не должен превышать 10 символов")]
+ string RoomNumber,
+
+ bool IsRepeat,
+
+ [Required(ErrorMessage = "Необходимо указать пациента")]
+ [Range(1, int.MaxValue, ErrorMessage = "Некорректный идентификатор пациента")]
+ int PatientId,
+
+ [Required(ErrorMessage = "Необходимо указать врача")]
+ [Range(1, int.MaxValue, ErrorMessage = "Некорректный идентификатор врача")]
+ int DoctorId
+);
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Application.Contracts/Appointments/AppointmentDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Appointments/AppointmentDto.cs
new file mode 100644
index 000000000..816c8ed39
--- /dev/null
+++ b/Polyclinic/Polyclinic.Application.Contracts/Appointments/AppointmentDto.cs
@@ -0,0 +1,19 @@
+namespace Polyclinic.Application.Contracts.Appointments;
+
+///
+/// DTO записи на прием для чтения
+///
+/// Уникальный идентификатор записи
+/// Дата и время приема
+/// Номер кабинета
+/// Признак повторного приема
+/// Идентификатор пациента
+/// Идентификатор врача
+public record AppointmentDto(
+ int Id,
+ DateTime AppointmentDateTime,
+ string RoomNumber,
+ bool IsRepeat,
+ int PatientId,
+ int DoctorId
+);
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Application.Contracts/Doctors/DoctorCreateUpdateDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Doctors/DoctorCreateUpdateDto.cs
new file mode 100644
index 000000000..d885fcada
--- /dev/null
+++ b/Polyclinic/Polyclinic.Application.Contracts/Doctors/DoctorCreateUpdateDto.cs
@@ -0,0 +1,32 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Polyclinic.Application.Contracts.Doctors;
+
+///
+/// DTO для создания и обновления врача
+///
+/// Номер паспорта
+/// ФИО врача
+/// Дата рождения
+/// Идентификатор специализации
+/// Стаж работы (лет)
+public record DoctorCreateUpdateDto(
+ [Required(ErrorMessage = "Номер паспорта обязателен")]
+ [MaxLength(20, ErrorMessage = "Номер паспорта не должен превышать 20 символов")]
+ string PassportNumber,
+
+ [Required(ErrorMessage = "ФИО обязательно")]
+ [MaxLength(150, ErrorMessage = "ФИО не должно превышать 150 символов")]
+ string FullName,
+
+ [Required(ErrorMessage = "Дата рождения обязательна")]
+ [DataType(DataType.Date)]
+ DateTime BirthDate,
+
+ [Required(ErrorMessage = "Необходимо указать специализацию")]
+ [Range(1, int.MaxValue, ErrorMessage = "Некорректный идентификатор специализации")]
+ int SpecializationId,
+
+ [Range(0, 100, ErrorMessage = "Стаж работы должен быть от 0 до 100 лет")]
+ int ExperienceYears
+);
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Application.Contracts/Doctors/DoctorDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Doctors/DoctorDto.cs
new file mode 100644
index 000000000..54a669b76
--- /dev/null
+++ b/Polyclinic/Polyclinic.Application.Contracts/Doctors/DoctorDto.cs
@@ -0,0 +1,19 @@
+namespace Polyclinic.Application.Contracts.Doctors;
+
+///
+/// DTO врача для чтения
+///
+/// Уникальный идентификатор врача
+/// Номер паспорта
+/// ФИО врача
+/// Дата рождения
+/// Идентификатор специализации
+/// Стаж работы (лет)
+public record DoctorDto(
+ int Id,
+ string PassportNumber,
+ string FullName,
+ DateTime BirthDate,
+ int SpecializationId,
+ int ExperienceYears
+);
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Application.Contracts/IAnalyticsService.cs b/Polyclinic/Polyclinic.Application.Contracts/IAnalyticsService.cs
new file mode 100644
index 000000000..b00122bb1
--- /dev/null
+++ b/Polyclinic/Polyclinic.Application.Contracts/IAnalyticsService.cs
@@ -0,0 +1,58 @@
+using Polyclinic.Application.Contracts.Analytics;
+using Polyclinic.Application.Contracts.Appointments;
+using Polyclinic.Application.Contracts.Doctors;
+using Polyclinic.Application.Contracts.Patients;
+
+namespace Polyclinic.Application.Contracts;
+
+///
+/// Сервис для получения аналитических выборок и отчетов
+///
+public interface IAnalyticsService
+{
+ ///
+ /// Возвращает список врачей со стажем работы более указанного количества лет
+ ///
+ /// Минимальный стаж в годах (по умолчанию 10)
+ ///
+ /// Список врачей, удовлетворяющих условию
+ ///
+ public Task> GetDoctorsWithExperienceAsync(int minExperienceYears = 10);
+
+ ///
+ /// Возвращает список пациентов, записанных к конкретному врачу, отсортированный по ФИО
+ ///
+ /// Идентификатор врача
+ ///
+ /// Отсортированный список пациентов
+ ///
+ public Task> GetPatientsByDoctorAsync(int doctorId);
+
+ ///
+ /// Возвращает статистику по повторным приемам за указанный месяц
+ ///
+ /// Дата, определяющая месяц и год выборки
+ ///
+ /// Статистика по приемам
+ ///
+ public Task GetMonthlyRepeatStatsAsync(DateTime targetDate);
+
+ ///
+ /// Возвращает пациентов старше определенного возраста, посетивших более одного врача, отсортированных по дате рождения
+ ///
+ /// Минимальный возраст пациента (по умолчанию 30)
+ ///
+ /// Список пациентов
+ ///
+ public Task> GetPatientsWithMultipleDoctorsAsync(int minAge = 30);
+
+ ///
+ /// Возвращает список приемов в указанном кабинете за текущий месяц (относительно переданной даты)
+ ///
+ /// Номер кабинета
+ /// Дата для определения месяца выборки
+ ///
+ /// Список приемов
+ ///
+ public Task> GetAppointmentsByRoomAsync(string roomNumber, DateTime targetDate);
+}
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Application.Contracts/IApplicationService.cs b/Polyclinic/Polyclinic.Application.Contracts/IApplicationService.cs
new file mode 100644
index 000000000..8eaa7c238
--- /dev/null
+++ b/Polyclinic/Polyclinic.Application.Contracts/IApplicationService.cs
@@ -0,0 +1,58 @@
+namespace Polyclinic.Application.Contracts;
+
+///
+/// Общий интерфейс сервиса приложения для выполнения CRUD-операций над DTO
+///
+/// Тип DTO для чтения данных
+/// Тип DTO для создания и обновления данных
+/// Тип первичного ключа сущности
+public interface IApplicationService
+ where TDto : class
+ where TCreateUpdateDto : class
+ where TKey : struct
+{
+ ///
+ /// Создаёт новую запись на основе DTO
+ ///
+ /// DTO с данными для создания
+ ///
+ /// Созданный объект DTO с заполненным идентификатором
+ ///
+ public Task Create(TCreateUpdateDto dto);
+
+ ///
+ /// Получает запись по идентификатору
+ ///
+ /// Идентификатор записи
+ ///
+ /// DTO записи, если она найдена; иначе null
+ ///
+ public Task Get(TKey dtoId);
+
+ ///
+ /// Получает список всех записей
+ ///
+ ///
+ /// Коллекция всех записей DTO
+ ///
+ public Task> GetAll();
+
+ ///
+ /// Обновляет существующую запись
+ ///
+ /// DTO с обновлёнными данными
+ /// Идентификатор обновляемой записи
+ ///
+ /// Обновлённый объект DTO
+ ///
+ public Task Update(TCreateUpdateDto dto, TKey dtoId);
+
+ ///
+ /// Удаляет запись по идентификатору
+ ///
+ /// Идентификатор удаляемой записи
+ ///
+ /// true, если запись была найдена и удалена; иначе false
+ ///
+ public Task Delete(TKey dtoId);
+}
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Application.Contracts/Patients/PatientCreateUpdateDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Patients/PatientCreateUpdateDto.cs
new file mode 100644
index 000000000..9e9f6b5be
--- /dev/null
+++ b/Polyclinic/Polyclinic.Application.Contracts/Patients/PatientCreateUpdateDto.cs
@@ -0,0 +1,47 @@
+using Polyclinic.Domain.Enums;
+using System.ComponentModel.DataAnnotations;
+
+namespace Polyclinic.Application.Contracts.Patients;
+
+///
+/// DTO для создания и обновления пациента
+///
+/// Номер паспорта
+/// ФИО пациента
+/// Пол пациента
+/// Дата рождения
+/// Адрес проживания
+/// Группа крови
+/// Резус-фактор
+/// Контактный телефон
+public record PatientCreateUpdateDto(
+ [Required(ErrorMessage = "Номер паспорта обязателен")]
+ [MaxLength(20, ErrorMessage = "Номер паспорта не должен превышать 20 символов")]
+ string PassportNumber,
+
+ [Required(ErrorMessage = "ФИО обязательно")]
+ [MaxLength(150, ErrorMessage = "ФИО не должно превышать 150 символов")]
+ string FullName,
+
+ [Required(ErrorMessage = "Пол обязателен")]
+ Gender Gender,
+
+ [Required(ErrorMessage = "Дата рождения обязательна")]
+ [DataType(DataType.Date)]
+ DateTime BirthDate,
+
+ [Required(ErrorMessage = "Адрес обязателен")]
+ [MaxLength(250, ErrorMessage = "Адрес не должен превышать 250 символов")]
+ string Address,
+
+ [Required(ErrorMessage = "Группа крови обязательна")]
+ BloodGroup BloodGroup,
+
+ [Required(ErrorMessage = "Резус-фактор обязателен")]
+ RhFactor RhFactor,
+
+ [Required(ErrorMessage = "Телефон обязателен")]
+ [Phone(ErrorMessage = "Некорректный формат телефона")]
+ [MaxLength(20, ErrorMessage = "Телефон не должен превышать 20 символов")]
+ string PhoneNumber
+);
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Application.Contracts/Patients/PatientDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Patients/PatientDto.cs
new file mode 100644
index 000000000..8332c45d6
--- /dev/null
+++ b/Polyclinic/Polyclinic.Application.Contracts/Patients/PatientDto.cs
@@ -0,0 +1,27 @@
+using Polyclinic.Domain.Enums;
+
+namespace Polyclinic.Application.Contracts.Patients;
+
+///
+/// DTO пациента для чтения
+///
+/// Уникальный идентификатор пациента
+/// Номер паспорта
+/// ФИО пациента
+/// Пол пациента
+/// Дата рождения
+/// Адрес проживания
+/// Группа крови
+/// Резус-фактор
+/// Контактный телефон
+public record PatientDto(
+ int Id,
+ string PassportNumber,
+ string FullName,
+ Gender Gender,
+ DateTime BirthDate,
+ string Address,
+ BloodGroup BloodGroup,
+ RhFactor RhFactor,
+ string PhoneNumber
+);
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Application.Contracts/Polyclinic.Application.Contracts.csproj b/Polyclinic/Polyclinic.Application.Contracts/Polyclinic.Application.Contracts.csproj
new file mode 100644
index 000000000..14a830ef0
--- /dev/null
+++ b/Polyclinic/Polyclinic.Application.Contracts/Polyclinic.Application.Contracts.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
diff --git a/Polyclinic/Polyclinic.Application.Contracts/Specializations/SpecializationCreateUpdateDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Specializations/SpecializationCreateUpdateDto.cs
new file mode 100644
index 000000000..251da3201
--- /dev/null
+++ b/Polyclinic/Polyclinic.Application.Contracts/Specializations/SpecializationCreateUpdateDto.cs
@@ -0,0 +1,22 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Polyclinic.Application.Contracts.Specializations;
+
+///
+/// DTO для создания и обновления специализации
+///
+/// Название специализации
+/// Описание специализации
+/// Код специализации
+public record SpecializationCreateUpdateDto(
+ [Required(ErrorMessage = "Название специализации обязательно")]
+ [MaxLength(100, ErrorMessage = "Название не должно превышать 100 символов")]
+ string Name,
+
+ [MaxLength(500, ErrorMessage = "Описание не должно превышать 500 символов")]
+ string Description,
+
+ [Required(ErrorMessage = "Код специализации обязателен")]
+ [MaxLength(20, ErrorMessage = "Код не должен превышать 20 символов")]
+ string Code
+);
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Application.Contracts/Specializations/SpecializationDto.cs b/Polyclinic/Polyclinic.Application.Contracts/Specializations/SpecializationDto.cs
new file mode 100644
index 000000000..0c25f8223
--- /dev/null
+++ b/Polyclinic/Polyclinic.Application.Contracts/Specializations/SpecializationDto.cs
@@ -0,0 +1,15 @@
+namespace Polyclinic.Application.Contracts.Specializations;
+
+///
+/// DTO специализации для чтения
+///
+/// Уникальный идентификатор специализации
+/// Название специализации
+/// Описание специализации
+/// Код специализации
+public record SpecializationDto(
+ int Id,
+ string Name,
+ string Description,
+ string Code
+);
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Application/Polyclinic.Application.csproj b/Polyclinic/Polyclinic.Application/Polyclinic.Application.csproj
new file mode 100644
index 000000000..14b3bad96
--- /dev/null
+++ b/Polyclinic/Polyclinic.Application/Polyclinic.Application.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Polyclinic/Polyclinic.Application/PolyclinicProfile.cs b/Polyclinic/Polyclinic.Application/PolyclinicProfile.cs
new file mode 100644
index 000000000..cc66113ad
--- /dev/null
+++ b/Polyclinic/Polyclinic.Application/PolyclinicProfile.cs
@@ -0,0 +1,29 @@
+using AutoMapper;
+using Polyclinic.Application.Contracts.Appointments;
+using Polyclinic.Application.Contracts.Doctors;
+using Polyclinic.Application.Contracts.Patients;
+using Polyclinic.Application.Contracts.Specializations;
+using Polyclinic.Domain.Entities;
+
+namespace Polyclinic.Application;
+
+///
+/// Профиль маппинга для преобразования данных между доменными сущностями и DTO
+///
+public class PolyclinicProfile : Profile
+{
+ public PolyclinicProfile()
+ {
+ CreateMap();
+ CreateMap();
+
+ CreateMap();
+ CreateMap();
+
+ CreateMap();
+ CreateMap();
+
+ CreateMap();
+ CreateMap();
+ }
+}
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Application/Services/AnalyticsSerivce.cs b/Polyclinic/Polyclinic.Application/Services/AnalyticsSerivce.cs
new file mode 100644
index 000000000..208a5ce9b
--- /dev/null
+++ b/Polyclinic/Polyclinic.Application/Services/AnalyticsSerivce.cs
@@ -0,0 +1,106 @@
+using AutoMapper;
+using Polyclinic.Application.Contracts;
+using Polyclinic.Application.Contracts.Analytics;
+using Polyclinic.Application.Contracts.Appointments;
+using Polyclinic.Application.Contracts.Doctors;
+using Polyclinic.Application.Contracts.Patients;
+using Polyclinic.Domain;
+using Polyclinic.Domain.Entities;
+
+namespace Polyclinic.Application.Services;
+
+///
+/// Сервис для получения аналитических данных и отчетов
+///
+public class AnalyticsService(
+ IRepository doctorRepository,
+ IRepository appointmentRepository,
+ IMapper mapper) : IAnalyticsService
+{
+ ///
+ public async Task> GetDoctorsWithExperienceAsync(int minExperienceYears = 10)
+ {
+ var doctors = await doctorRepository.ReadAll();
+
+ var filteredDoctors = doctors
+ .Where(d => d.ExperienceYears >= minExperienceYears)
+ .ToList();
+
+ return mapper.Map>(filteredDoctors);
+ }
+
+ ///
+ public async Task> GetPatientsByDoctorAsync(int doctorId)
+ {
+ var appointments = await appointmentRepository.ReadAll();
+
+ var patients = appointments
+ .Where(a => a.DoctorId == doctorId && a.Patient != null)
+ .Select(a => a.Patient!)
+ .DistinctBy(p => p.Id)
+ .OrderBy(p => p.FullName)
+ .ToList();
+
+ return mapper.Map>(patients);
+ }
+
+ ///
+ public async Task GetMonthlyRepeatStatsAsync(DateTime targetDate)
+ {
+ var appointments = await appointmentRepository.ReadAll();
+
+ var monthlyAppointments = appointments
+ .Where(a => a.AppointmentDateTime.Year == targetDate.Year
+ && a.AppointmentDateTime.Month == targetDate.Month)
+ .ToList();
+
+ var totalCount = monthlyAppointments.Count;
+ var repeatCount = monthlyAppointments.Count(a => a.IsRepeat);
+
+ return new MonthlyAppointmentStatsDto(
+ Year: targetDate.Year,
+ Month: targetDate.Month,
+ RepeatAppointmentCount: repeatCount,
+ TotalAppointmentCount: totalCount
+ );
+ }
+
+ ///
+ public async Task> GetPatientsWithMultipleDoctorsAsync(int minAge = 30)
+ {
+ var appointments = await appointmentRepository.ReadAll();
+ var today = DateTime.Today;
+
+ var resultPatients = appointments
+ .Where(a => a.Patient != null)
+ .GroupBy(a => a.Patient)
+ .Where(g =>
+ {
+ var patient = g.Key!;
+ var isOldEnough = patient.GetAge(today) > minAge;
+
+ var hasMultipleDoctors = g.Select(a => a.DoctorId).Distinct().Count() > 1;
+
+ return isOldEnough && hasMultipleDoctors;
+ })
+ .Select(g => g.Key!)
+ .OrderBy(p => p.BirthDate)
+ .ToList();
+
+ return mapper.Map>(resultPatients);
+ }
+
+ ///
+ public async Task> GetAppointmentsByRoomAsync(string roomNumber, DateTime targetDate)
+ {
+ var appointments = await appointmentRepository.ReadAll();
+
+ var filtered = appointments
+ .Where(a => a.RoomNumber == roomNumber
+ && a.AppointmentDateTime.Year == targetDate.Year
+ && a.AppointmentDateTime.Month == targetDate.Month)
+ .ToList();
+
+ return mapper.Map>(filtered);
+ }
+}
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Application/Services/AppointmentService.cs b/Polyclinic/Polyclinic.Application/Services/AppointmentService.cs
new file mode 100644
index 000000000..58e151b61
--- /dev/null
+++ b/Polyclinic/Polyclinic.Application/Services/AppointmentService.cs
@@ -0,0 +1,69 @@
+using AutoMapper;
+using Polyclinic.Application.Contracts;
+using Polyclinic.Application.Contracts.Appointments;
+using Polyclinic.Domain;
+using Polyclinic.Domain.Entities;
+
+namespace Polyclinic.Application.Services;
+
+///
+/// Сервис для управления записями на прием
+///
+public class AppointmentService(
+ IRepository appointmentRepository,
+ IRepository doctorRepository,
+ IRepository patientRepository,
+ IMapper mapper)
+ : IApplicationService
+{
+ ///
+ public async Task Create(AppointmentCreateUpdateDto dto)
+ {
+ _ = await patientRepository.Read(dto.PatientId)
+ ?? throw new KeyNotFoundException($"Пациент с id {dto.PatientId} не найден.");
+
+ _ = await doctorRepository.Read(dto.DoctorId)
+ ?? throw new KeyNotFoundException($"Врач с id {dto.DoctorId} не найден.");
+
+ var entity = mapper.Map(dto);
+ var result = await appointmentRepository.Create(entity);
+ return mapper.Map(result);
+ }
+
+ ///
+ public async Task Get(int dtoId)
+ {
+ var entity = await appointmentRepository.Read(dtoId)
+ ?? throw new KeyNotFoundException($"Запись на прием с id {dtoId} не найдена.");
+
+ return mapper.Map(entity);
+ }
+
+ ///
+ public async Task> GetAll()
+ {
+ var entities = await appointmentRepository.ReadAll();
+ return mapper.Map>(entities);
+ }
+
+ ///
+ public async Task Update(AppointmentCreateUpdateDto dto, int dtoId)
+ {
+ _ = await patientRepository.Read(dto.PatientId)
+ ?? throw new KeyNotFoundException($"Пациент с id {dto.PatientId} не найден.");
+
+ _ = await doctorRepository.Read(dto.DoctorId)
+ ?? throw new KeyNotFoundException($"Врач с id {dto.DoctorId} не найден.");
+
+ var entity = await appointmentRepository.Read(dtoId)
+ ?? throw new KeyNotFoundException($"Запись на прием с id {dtoId} не найдена.");
+
+ mapper.Map(dto, entity);
+
+ var result = await appointmentRepository.Update(entity);
+ return mapper.Map(result);
+ }
+
+ ///
+ public async Task Delete(int dtoId) => await appointmentRepository.Delete(dtoId);
+}
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Application/Services/DoctorService.cs b/Polyclinic/Polyclinic.Application/Services/DoctorService.cs
new file mode 100644
index 000000000..cc6f59cf9
--- /dev/null
+++ b/Polyclinic/Polyclinic.Application/Services/DoctorService.cs
@@ -0,0 +1,62 @@
+using AutoMapper;
+using Polyclinic.Application.Contracts;
+using Polyclinic.Application.Contracts.Doctors;
+using Polyclinic.Domain;
+using Polyclinic.Domain.Entities;
+
+namespace Polyclinic.Application.Services;
+
+///
+/// Сервис для управления врачами
+///
+public class DoctorService(
+ IRepository doctorRepository,
+ IRepository specializationRepository,
+ IMapper mapper)
+ : IApplicationService
+{
+ ///
+ public async Task Create(DoctorCreateUpdateDto dto)
+ {
+ _ = await specializationRepository.Read(dto.SpecializationId)
+ ?? throw new KeyNotFoundException($"Специализация с id {dto.SpecializationId} не найдена.");
+
+ var entity = mapper.Map(dto);
+ var result = await doctorRepository.Create(entity);
+ return mapper.Map(result);
+ }
+
+ ///
+ public async Task Get(int dtoId)
+ {
+ var entity = await doctorRepository.Read(dtoId)
+ ?? throw new KeyNotFoundException($"Врач с id {dtoId} не найден.");
+
+ return mapper.Map(entity);
+ }
+
+ ///
+ public async Task> GetAll()
+ {
+ var entities = await doctorRepository.ReadAll();
+ return mapper.Map>(entities);
+ }
+
+ ///
+ public async Task Update(DoctorCreateUpdateDto dto, int dtoId)
+ {
+ _ = await specializationRepository.Read(dto.SpecializationId)
+ ?? throw new KeyNotFoundException($"Специализация с id {dto.SpecializationId} не найдена.");
+
+ var entity = await doctorRepository.Read(dtoId)
+ ?? throw new KeyNotFoundException($"Врач с id {dtoId} не найден.");
+
+ mapper.Map(dto, entity);
+
+ var result = await doctorRepository.Update(entity);
+ return mapper.Map(result);
+ }
+
+ ///
+ public async Task Delete(int dtoId) => await doctorRepository.Delete(dtoId);
+}
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Application/Services/PatientService.cs b/Polyclinic/Polyclinic.Application/Services/PatientService.cs
new file mode 100644
index 000000000..4b04075bd
--- /dev/null
+++ b/Polyclinic/Polyclinic.Application/Services/PatientService.cs
@@ -0,0 +1,55 @@
+using AutoMapper;
+using Polyclinic.Application.Contracts;
+using Polyclinic.Application.Contracts.Patients;
+using Polyclinic.Domain;
+using Polyclinic.Domain.Entities;
+
+namespace Polyclinic.Application.Services;
+
+///
+/// Сервис для управления пациентами
+///
+public class PatientService(
+ IRepository repository,
+ IMapper mapper)
+ : IApplicationService
+{
+ ///
+ public async Task Create(PatientCreateUpdateDto dto)
+ {
+ var entity = mapper.Map(dto);
+ var result = await repository.Create(entity);
+ return mapper.Map(result);
+ }
+
+ ///
+ public async Task Get(int dtoId)
+ {
+ var entity = await repository.Read(dtoId)
+ ?? throw new KeyNotFoundException($"Пациент с id {dtoId} не найден.");
+
+ return mapper.Map(entity);
+ }
+
+ ///
+ public async Task> GetAll()
+ {
+ var entities = await repository.ReadAll();
+ return mapper.Map>(entities);
+ }
+
+ ///
+ public async Task Update(PatientCreateUpdateDto dto, int dtoId)
+ {
+ var entity = await repository.Read(dtoId)
+ ?? throw new KeyNotFoundException($"Пациент с id {dtoId} не найден.");
+
+ mapper.Map(dto, entity);
+
+ var result = await repository.Update(entity);
+ return mapper.Map(result);
+ }
+
+ ///
+ public async Task Delete(int dtoId) => await repository.Delete(dtoId);
+}
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Application/Services/SpecializationService.cs b/Polyclinic/Polyclinic.Application/Services/SpecializationService.cs
new file mode 100644
index 000000000..e698a3d6c
--- /dev/null
+++ b/Polyclinic/Polyclinic.Application/Services/SpecializationService.cs
@@ -0,0 +1,55 @@
+using AutoMapper;
+using Polyclinic.Application.Contracts;
+using Polyclinic.Application.Contracts.Specializations;
+using Polyclinic.Domain;
+using Polyclinic.Domain.Entities;
+
+namespace Polyclinic.Application.Services;
+
+///
+/// Сервис для управления специализациями
+///
+public class SpecializationService(
+ IRepository repository,
+ IMapper mapper)
+ : IApplicationService
+{
+ ///
+ public async Task Create(SpecializationCreateUpdateDto dto)
+ {
+ var entity = mapper.Map(dto);
+ var result = await repository.Create(entity);
+ return mapper.Map(result);
+ }
+
+ ///
+ public async Task Get(int dtoId)
+ {
+ var entity = await repository.Read(dtoId)
+ ?? throw new KeyNotFoundException($"Специализация с id {dtoId} не найдена.");
+
+ return mapper.Map(entity);
+ }
+
+ ///
+ public async Task> GetAll()
+ {
+ var entities = await repository.ReadAll();
+ return mapper.Map>(entities);
+ }
+
+ ///
+ public async Task Update(SpecializationCreateUpdateDto dto, int dtoId)
+ {
+ var entity = await repository.Read(dtoId)
+ ?? throw new KeyNotFoundException($"Специализация с id {dtoId} не найдена.");
+
+ mapper.Map(dto, entity);
+
+ var result = await repository.Update(entity);
+ return mapper.Map(result);
+ }
+
+ ///
+ public async Task Delete(int dtoId) => await repository.Delete(dtoId);
+}
\ 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/IRepository.cs b/Polyclinic/Polyclinic.Domain/IRepository.cs
new file mode 100644
index 000000000..4f4dc5f08
--- /dev/null
+++ b/Polyclinic/Polyclinic.Domain/IRepository.cs
@@ -0,0 +1,55 @@
+namespace Polyclinic.Domain;
+
+///
+/// Интерфейс репозитория, инкапсулирующий CRUD-операции над сущностью доменной модели
+///
+/// Тип доменной сущности, над которой выполняются операции репозитория
+/// Тип первичного ключа (идентификатора) сущности
+public interface IRepository
+ where TEntity : class
+ where TKey : struct
+{
+ ///
+ /// Создаёт новую сущность и сохраняет её в источнике данных
+ ///
+ /// Экземпляр новой сущности для сохранения
+ ///
+ /// Созданная сущность
+ ///
+ public Task Create(TEntity entity);
+
+ ///
+ /// Возвращает сущность по её идентификатору
+ ///
+ /// Идентификатор искомой сущности
+ ///
+ /// Сущность, если она найдена; иначе null
+ ///
+ public Task Read(TKey entityId);
+
+ ///
+ /// Возвращает полный список сущностей данного типа
+ ///
+ ///
+ /// Список сущностей
+ ///
+ public Task> ReadAll();
+
+ ///
+ /// Обновляет существующую сущность в источнике данных
+ ///
+ /// Сущность с актуальными значениями полей
+ ///
+ /// Обновлённая сущность
+ ///
+ public Task Update(TEntity entity);
+
+ ///
+ /// Удаляет сущность по её идентификатору
+ ///
+ /// Идентификатор удаляемой сущности
+ ///
+ /// true, если сущность была найдена и удалена; иначе false
+ ///
+ public Task Delete(TKey entityId);
+}
\ No newline at end of file
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.Domain/PolyclinicFixture.cs b/Polyclinic/Polyclinic.Domain/PolyclinicFixture.cs
new file mode 100644
index 000000000..e7b6ea4b1
--- /dev/null
+++ b/Polyclinic/Polyclinic.Domain/PolyclinicFixture.cs
@@ -0,0 +1,419 @@
+using Polyclinic.Domain.Entities;
+using Polyclinic.Domain.Enums;
+
+namespace Polyclinic.Domain;
+
+///
+/// Фикстура с тестовыми данными для поликлиники
+///
+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();
+ }
+
+ 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.Generator.RabbitMq.Host/Controllers/GeneratorController.cs b/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Controllers/GeneratorController.cs
new file mode 100644
index 000000000..02865cfd7
--- /dev/null
+++ b/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Controllers/GeneratorController.cs
@@ -0,0 +1,54 @@
+using Microsoft.AspNetCore.Mvc;
+using Polyclinic.Application.Contracts.Appointments;
+using Polyclinic.Generator.RabbitMq.Host.Services;
+
+namespace Polyclinic.Generator.RabbitMq.Host.Controllers;
+
+///
+/// Контроллер для управления генерацией тестовых данных
+///
+/// Сервис генерации фейковых данных
+/// Сервис публикации сообщений в RabbitMQ
+/// Логгер
+[Route("api/[controller]")]
+[ApiController]
+public class GeneratorController(
+ AppointmentGenerator generator,
+ AppointmentProducer producer,
+ ILogger logger) : ControllerBase
+{
+ ///
+ /// Инициирует генерацию указанного количества записей на прием и отправляет их в очередь RabbitMQ
+ ///
+ /// Количество записей для генерации (по умолчанию 10)
+ /// Токен отмены операции
+ /// Список сгенерированных и отправленных записей
+ [HttpPost("generate")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task>> Generate(
+ [FromQuery] int count = 10,
+ CancellationToken cancellationToken = default)
+ {
+ logger.LogInformation("Запуск генерации {Count} записей на прием...", count);
+
+ try
+ {
+ var appointments = generator.Generate(count).ToList();
+
+ foreach (var appointment in appointments)
+ {
+ await producer.PublishAsync(appointment, cancellationToken);
+ }
+
+ logger.LogInformation("Успешно сгенерировано и отправлено {Count} записей.", appointments.Count);
+
+ return Ok(appointments);
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Ошибка при генерации или отправке данных в RabbitMQ.");
+ return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
+ }
+ }
+}
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Options/GeneratorOptions.cs b/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Options/GeneratorOptions.cs
new file mode 100644
index 000000000..0be9d2db1
--- /dev/null
+++ b/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Options/GeneratorOptions.cs
@@ -0,0 +1,53 @@
+namespace Polyclinic.Generator.RabbitMq.Host.Options;
+
+///
+/// Настройки генератора данных
+/// Определяют границы значений для создаваемых сущностей
+///
+public class GeneratorOptions
+{
+ ///
+ /// Минимальный идентификатор пациента
+ ///
+ public int MinPatientId { get; set; } = 1;
+
+ ///
+ /// Максимальный идентификатор пациента
+ ///
+ public int MaxPatientId { get; set; } = 12;
+
+ ///
+ /// Минимальный идентификатор врача
+ ///
+ public int MinDoctorId { get; set; } = 1;
+
+ ///
+ /// Максимальный идентификатор врача
+ ///
+ public int MaxDoctorId { get; set; } = 10;
+
+ ///
+ /// Дата начала периода генерации записей
+ ///
+ public DateTime StartDate { get; set; } = new DateTime(2025, 2, 18, 12, 0, 0);
+
+ ///
+ /// Дата окончания периода генерации записей
+ ///
+ public DateTime EndDate { get; set; } = new DateTime(2025, 3, 18, 12, 0, 0);
+
+ ///
+ /// Минимальный номер кабинета
+ ///
+ public int MinRoomNumber { get; set; } = 100;
+
+ ///
+ /// Максимальный номер кабинета
+ ///
+ public int MaxRoomNumber { get; set; } = 599;
+
+ ///
+ /// Вероятность того, что прием является повторным (от 0.0 до 1.0)
+ ///
+ public float RepeatProbability { get; set; } = 0.3f;
+}
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Options/RabbitMqOptions.cs b/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Options/RabbitMqOptions.cs
new file mode 100644
index 000000000..ccb83bbc8
--- /dev/null
+++ b/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Options/RabbitMqOptions.cs
@@ -0,0 +1,22 @@
+namespace Polyclinic.Generator.RabbitMq.Host.Options;
+
+///
+/// Настройки конфигурации для подключения и работы с RabbitMQ
+///
+public class RabbitMqOptions
+{
+ ///
+ /// Имя очереди для отправки сообщений
+ ///
+ public required string QueueName { get; set; } = "appointment-contracts";
+
+ ///
+ /// Количество повторных попыток обработки сообщения при возникновении ошибок
+ ///
+ public int RetryCount { get; set; } = 5;
+
+ ///
+ /// Задержка между повторными попытками в миллисекундах
+ ///
+ public int RetryDelayMs { get; set; } = 1000;
+}
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Polyclinic.Generator.RabbitMq.Host.csproj b/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Polyclinic.Generator.RabbitMq.Host.csproj
new file mode 100644
index 000000000..8801d7e0b
--- /dev/null
+++ b/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Polyclinic.Generator.RabbitMq.Host.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Program.cs b/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Program.cs
new file mode 100644
index 000000000..4f20d24ee
--- /dev/null
+++ b/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Program.cs
@@ -0,0 +1,50 @@
+using Polyclinic.Generator.RabbitMq.Host.Options;
+using Polyclinic.Generator.RabbitMq.Host.Services;
+using Polyclinic.ServiceDefaults;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+
+builder.AddRabbitMQClient("rabbitMqConnection");
+
+builder.Services.Configure(builder.Configuration.GetSection("RabbitMq"));
+builder.Services.Configure(builder.Configuration.GetSection("Generator"));
+
+builder.Services.AddSingleton();
+builder.Services.AddSingleton();
+
+builder.Services.AddControllers();
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(c =>
+{
+ var assemblies = AppDomain.CurrentDomain.GetAssemblies()
+ .Where(a => a.GetName().Name!.StartsWith("Polyclinic"))
+ .Distinct();
+
+ foreach (var assembly in assemblies)
+ {
+ var xmlFile = $"{assembly.GetName().Name}.xml";
+ var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
+ if (File.Exists(xmlPath))
+ c.IncludeXmlComments(xmlPath);
+ }
+});
+
+var app = builder.Build();
+
+app.MapDefaultEndpoints();
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+
+app.UseAuthorization();
+
+app.MapControllers();
+
+app.Run();
diff --git a/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Properties/launchSettings.json b/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Properties/launchSettings.json
new file mode 100644
index 000000000..146c903da
--- /dev/null
+++ b/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:51800",
+ "sslPort": 44382
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5212",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7255;http://localhost:5212",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Services/AppointmentGenerator.cs b/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Services/AppointmentGenerator.cs
new file mode 100644
index 000000000..b429d52ad
--- /dev/null
+++ b/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Services/AppointmentGenerator.cs
@@ -0,0 +1,42 @@
+using Bogus;
+using Microsoft.Extensions.Options;
+using Polyclinic.Application.Contracts.Appointments;
+using Polyclinic.Generator.RabbitMq.Host.Options;
+
+namespace Polyclinic.Generator.RabbitMq.Host.Services;
+
+///
+/// Сервис для генерации фейковых данных о записях на прием
+///
+public class AppointmentGenerator
+{
+ private readonly Faker _faker;
+
+ ///
+ /// Инициализирует новый экземпляр генератора с заданными настройками
+ ///
+ /// Настройки генерации (диапазоны ID, дат и т.д.)
+ public AppointmentGenerator(IOptions options)
+ {
+ var configuration = options.Value;
+
+ _faker = new Faker()
+ .CustomInstantiator(f => new AppointmentCreateUpdateDto(
+ AppointmentDateTime: f.Date.Between(configuration.StartDate, configuration.EndDate),
+ RoomNumber: f.Random.Int(configuration.MinRoomNumber, configuration.MaxRoomNumber).ToString(),
+ IsRepeat: f.Random.Bool(configuration.RepeatProbability),
+ PatientId: f.Random.Int(configuration.MinPatientId, configuration.MaxPatientId),
+ DoctorId: f.Random.Int(configuration.MinDoctorId, configuration.MaxDoctorId)
+ ));
+ }
+
+ ///
+ /// Генерирует указанное количество записей на прием
+ ///
+ /// Количество записей для генерации
+ /// Коллекция DTO для создания записей
+ public IEnumerable Generate(int count)
+ {
+ return _faker.Generate(count);
+ }
+}
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Services/AppointmentProducer.cs b/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Services/AppointmentProducer.cs
new file mode 100644
index 000000000..2e93b9cb6
--- /dev/null
+++ b/Polyclinic/Polyclinic.Generator.RabbitMq.Host/Services/AppointmentProducer.cs
@@ -0,0 +1,83 @@
+using Microsoft.Extensions.Options;
+using Polyclinic.Application.Contracts.Appointments;
+using Polyclinic.Generator.RabbitMq.Host.Options;
+using RabbitMQ.Client;
+using System.Text;
+using System.Text.Json;
+
+namespace Polyclinic.Generator.RabbitMq.Host.Services;
+
+///
+/// Сервис для публикации сообщений о создании записей на прием в RabbitMQ
+///
+/// Активное соединение с RabbitMQ
+/// Настройки очереди и повторных попыток
+/// Логгер для записи событий и ошибок
+public class AppointmentProducer(
+ IConnection connection,
+ IOptions options,
+ ILogger logger) : IDisposable
+{
+ private readonly RabbitMqOptions _options = options.Value;
+ private IChannel? _channel;
+
+ ///
+ /// Публикует сообщение с данными о записи на прием в очередь
+ ///
+ /// DTO с данными записи
+ /// Токен отмены
+ public async Task PublishAsync(AppointmentCreateUpdateDto appointment, CancellationToken token = default)
+ {
+ await EnsureChannelAsync(token);
+
+ var json = JsonSerializer.Serialize(appointment);
+ var body = Encoding.UTF8.GetBytes(json);
+
+ var properties = new BasicProperties
+ {
+ DeliveryMode = DeliveryModes.Persistent,
+ ContentType = "application/json"
+ };
+
+ await _channel!.BasicPublishAsync(
+ exchange: string.Empty,
+ routingKey: _options.QueueName,
+ mandatory: false,
+ basicProperties: properties,
+ body: body,
+ cancellationToken: token);
+
+ logger.LogInformation("Отправлено сообщение в очередь {Queue}: {Json}.", _options.QueueName, json);
+ }
+
+ ///
+ /// Инициализирует канал и объявляет очередь, если это еще не было сделано
+ ///
+ private async Task EnsureChannelAsync(CancellationToken token)
+ {
+ if (_channel is not null) return;
+
+ logger.LogInformation("Создание RabbitMQ канала для очереди {QueueName}.", _options.QueueName);
+
+ _channel = await connection.CreateChannelAsync(cancellationToken: token);
+
+ await _channel.QueueDeclareAsync(
+ queue: _options.QueueName,
+ durable: true,
+ exclusive: false,
+ autoDelete: false,
+ arguments: null,
+ cancellationToken: token);
+
+ logger.LogInformation("Канал RabbitMQ успешно создан.");
+ }
+
+ ///
+ /// Освобождает ресурсы канала
+ ///
+ public void Dispose()
+ {
+ _channel?.Dispose();
+ GC.SuppressFinalize(this);
+ }
+}
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Generator.RabbitMq.Host/appsettings.Development.json b/Polyclinic/Polyclinic.Generator.RabbitMq.Host/appsettings.Development.json
new file mode 100644
index 000000000..0c208ae91
--- /dev/null
+++ b/Polyclinic/Polyclinic.Generator.RabbitMq.Host/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/Polyclinic/Polyclinic.Generator.RabbitMq.Host/appsettings.json b/Polyclinic/Polyclinic.Generator.RabbitMq.Host/appsettings.json
new file mode 100644
index 000000000..00233a8b2
--- /dev/null
+++ b/Polyclinic/Polyclinic.Generator.RabbitMq.Host/appsettings.json
@@ -0,0 +1,25 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "RabbitMq": {
+ "QueueName": "appointment-contracts",
+ "RetryCount": 5,
+ "RetryDelayMs": 1000
+ },
+ "Generator": {
+ "MinPatientId": 1,
+ "MaxPatientId": 12,
+ "MinDoctorId": 1,
+ "MaxDoctorId": 10,
+ "StartDate": "2025-02-18T12:00:00",
+ "EndDate": "2025-03-18T12:00:00",
+ "MinRoomNumber": 100,
+ "MaxRoomNumber": 599,
+ "RepeatProbability": 0.3
+ }
+}
diff --git a/Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/20260216193102_InitialCreate.Designer.cs b/Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/20260216193102_InitialCreate.Designer.cs
new file mode 100644
index 000000000..f8d283b86
--- /dev/null
+++ b/Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/20260216193102_InitialCreate.Designer.cs
@@ -0,0 +1,627 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Polyclinic.Infrastructure.EfCore;
+
+#nullable disable
+
+namespace Polyclinic.Infrastructure.EfCore.Migrations
+{
+ [DbContext(typeof(PolyclinicDbContext))]
+ [Migration("20260216193102_InitialCreate")]
+ partial class InitialCreate
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.13")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Polyclinic.Domain.Entities.Appointment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AppointmentDateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("DoctorId")
+ .HasColumnType("int");
+
+ b.Property("IsRepeat")
+ .HasColumnType("bit");
+
+ b.Property("PatientId")
+ .HasColumnType("int");
+
+ b.Property("RoomNumber")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("nvarchar(10)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DoctorId");
+
+ b.HasIndex("PatientId");
+
+ b.ToTable("Appointments");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ AppointmentDateTime = new DateTime(2026, 2, 5, 10, 0, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 1,
+ IsRepeat = false,
+ PatientId = 1,
+ RoomNumber = "101"
+ },
+ new
+ {
+ Id = 2,
+ AppointmentDateTime = new DateTime(2026, 2, 5, 11, 0, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 1,
+ IsRepeat = true,
+ PatientId = 2,
+ RoomNumber = "101"
+ },
+ new
+ {
+ Id = 3,
+ AppointmentDateTime = new DateTime(2026, 2, 10, 14, 30, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 2,
+ IsRepeat = false,
+ PatientId = 3,
+ RoomNumber = "202"
+ },
+ new
+ {
+ Id = 4,
+ AppointmentDateTime = new DateTime(2026, 2, 15, 9, 15, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 3,
+ IsRepeat = false,
+ PatientId = 4,
+ RoomNumber = "303"
+ },
+ new
+ {
+ Id = 5,
+ AppointmentDateTime = new DateTime(2026, 2, 12, 13, 30, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 1,
+ IsRepeat = false,
+ PatientId = 8,
+ RoomNumber = "101"
+ },
+ new
+ {
+ Id = 6,
+ AppointmentDateTime = new DateTime(2026, 2, 16, 9, 30, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 1,
+ IsRepeat = false,
+ PatientId = 11,
+ RoomNumber = "101"
+ },
+ new
+ {
+ Id = 7,
+ AppointmentDateTime = new DateTime(2026, 1, 15, 10, 0, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 2,
+ IsRepeat = true,
+ PatientId = 6,
+ RoomNumber = "101"
+ },
+ new
+ {
+ Id = 8,
+ AppointmentDateTime = new DateTime(2026, 1, 20, 11, 0, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 2,
+ IsRepeat = true,
+ PatientId = 7,
+ RoomNumber = "202"
+ },
+ new
+ {
+ Id = 9,
+ AppointmentDateTime = new DateTime(2026, 1, 5, 9, 0, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 3,
+ IsRepeat = false,
+ PatientId = 8,
+ RoomNumber = "303"
+ },
+ new
+ {
+ Id = 10,
+ AppointmentDateTime = new DateTime(2026, 2, 8, 9, 0, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 2,
+ IsRepeat = false,
+ PatientId = 5,
+ RoomNumber = "102"
+ },
+ new
+ {
+ Id = 11,
+ AppointmentDateTime = new DateTime(2026, 2, 22, 11, 30, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 6,
+ IsRepeat = false,
+ PatientId = 5,
+ RoomNumber = "606"
+ });
+ });
+
+ modelBuilder.Entity("Polyclinic.Domain.Entities.Doctor", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("BirthDate")
+ .HasColumnType("datetime2");
+
+ b.Property("ExperienceYears")
+ .HasColumnType("int");
+
+ b.Property("FullName")
+ .IsRequired()
+ .HasMaxLength(150)
+ .HasColumnType("nvarchar(150)");
+
+ b.Property("PassportNumber")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.Property("SpecializationId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SpecializationId");
+
+ b.ToTable("Doctors");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ BirthDate = new DateTime(1980, 5, 15, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ ExperienceYears = 15,
+ FullName = "Иванов Иван Иванович",
+ PassportNumber = "4501 123456",
+ SpecializationId = 1
+ },
+ new
+ {
+ Id = 2,
+ BirthDate = new DateTime(1975, 8, 22, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ ExperienceYears = 20,
+ FullName = "Петров Петр Петрович",
+ PassportNumber = "4502 234567",
+ SpecializationId = 2
+ },
+ new
+ {
+ Id = 3,
+ BirthDate = new DateTime(1985, 3, 10, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ ExperienceYears = 12,
+ FullName = "Сидорова Анна Сергеевна",
+ PassportNumber = "4503 345678",
+ SpecializationId = 3
+ },
+ new
+ {
+ Id = 4,
+ BirthDate = new DateTime(1990, 11, 30, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ ExperienceYears = 8,
+ FullName = "Козлов Дмитрий Николаевич",
+ PassportNumber = "4504 456789",
+ SpecializationId = 4
+ },
+ new
+ {
+ Id = 5,
+ BirthDate = new DateTime(1982, 7, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ ExperienceYears = 14,
+ FullName = "Морозова Елена Владимировна",
+ PassportNumber = "4505 567890",
+ SpecializationId = 5
+ },
+ new
+ {
+ Id = 6,
+ BirthDate = new DateTime(1978, 9, 25, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ ExperienceYears = 18,
+ FullName = "Волков Андрей Игоревич",
+ PassportNumber = "4506 678901",
+ SpecializationId = 6
+ },
+ new
+ {
+ Id = 7,
+ BirthDate = new DateTime(1988, 2, 14, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ ExperienceYears = 10,
+ FullName = "Соколова Татьяна Александровна",
+ PassportNumber = "4507 789012",
+ SpecializationId = 7
+ },
+ new
+ {
+ Id = 8,
+ BirthDate = new DateTime(1992, 6, 5, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ ExperienceYears = 6,
+ FullName = "Лебедев Михаил Сергеевич",
+ PassportNumber = "4508 890123",
+ SpecializationId = 8
+ },
+ new
+ {
+ Id = 9,
+ BirthDate = new DateTime(1983, 12, 3, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ ExperienceYears = 13,
+ FullName = "Николаева Ольга Викторовна",
+ PassportNumber = "4509 901234",
+ SpecializationId = 9
+ },
+ new
+ {
+ Id = 10,
+ BirthDate = new DateTime(1970, 4, 20, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ ExperienceYears = 25,
+ FullName = "Федоров Алексей Павлович",
+ PassportNumber = "4510 012345",
+ SpecializationId = 10
+ });
+ });
+
+ modelBuilder.Entity("Polyclinic.Domain.Entities.Patient", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .IsRequired()
+ .HasMaxLength(250)
+ .HasColumnType("nvarchar(250)");
+
+ b.Property("BirthDate")
+ .HasColumnType("datetime2");
+
+ b.Property("BloodGroup")
+ .HasColumnType("int");
+
+ b.Property("FullName")
+ .IsRequired()
+ .HasMaxLength(150)
+ .HasColumnType("nvarchar(150)");
+
+ b.Property("Gender")
+ .HasColumnType("int");
+
+ b.Property("PassportNumber")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.Property("PhoneNumber")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.Property("RhFactor")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.ToTable("Patients");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ Address = "ул. Ленина, д. 10, кв. 25",
+ BirthDate = new DateTime(1990, 5, 15, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 1,
+ FullName = "Смирнов Алексей Викторович",
+ Gender = 1,
+ PassportNumber = "6001 123456",
+ PhoneNumber = "+7 (999) 123-45-67",
+ RhFactor = 0
+ },
+ new
+ {
+ Id = 2,
+ Address = "ул. Гагарина, д. 5, кв. 12",
+ BirthDate = new DateTime(1985, 8, 22, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 0,
+ FullName = "Кузнецова Елена Дмитриевна",
+ Gender = 2,
+ PassportNumber = "6002 234567",
+ PhoneNumber = "+7 (999) 234-56-78",
+ RhFactor = 0
+ },
+ new
+ {
+ Id = 3,
+ Address = "пр. Мира, д. 15, кв. 7",
+ BirthDate = new DateTime(1978, 3, 10, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 2,
+ FullName = "Попов Сергей Иванович",
+ Gender = 1,
+ PassportNumber = "6003 345678",
+ PhoneNumber = "+7 (999) 345-67-89",
+ RhFactor = 1
+ },
+ new
+ {
+ Id = 4,
+ Address = "ул. Советская, д. 8, кв. 42",
+ BirthDate = new DateTime(1995, 11, 30, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 3,
+ FullName = "Васильева Мария Петровна",
+ Gender = 2,
+ PassportNumber = "6004 456789",
+ PhoneNumber = "+7 (999) 456-78-90",
+ RhFactor = 0
+ },
+ new
+ {
+ Id = 5,
+ Address = "ул. Пушкина, д. 3, кв. 56",
+ BirthDate = new DateTime(1982, 7, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 1,
+ FullName = "Соколов Андрей Николаевич",
+ Gender = 1,
+ PassportNumber = "6005 567890",
+ PhoneNumber = "+7 (999) 567-89-01",
+ RhFactor = 1
+ },
+ new
+ {
+ Id = 6,
+ Address = "пр. Ленинградский, д. 22, кв. 15",
+ BirthDate = new DateTime(1975, 9, 25, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 0,
+ FullName = "Михайлова Анна Сергеевна",
+ Gender = 2,
+ PassportNumber = "6006 678901",
+ PhoneNumber = "+7 (999) 678-90-12",
+ RhFactor = 0
+ },
+ new
+ {
+ Id = 7,
+ Address = "ул. Кирова, д. 12, кв. 8",
+ BirthDate = new DateTime(1988, 2, 14, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 2,
+ FullName = "Новиков Денис Александрович",
+ Gender = 1,
+ PassportNumber = "6007 789012",
+ PhoneNumber = "+7 (999) 789-01-23",
+ RhFactor = 0
+ },
+ new
+ {
+ Id = 8,
+ Address = "ул. Садовая, д. 7, кв. 31",
+ BirthDate = new DateTime(1992, 6, 5, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 3,
+ FullName = "Морозова Татьяна Владимировна",
+ Gender = 2,
+ PassportNumber = "6008 890123",
+ PhoneNumber = "+7 (999) 890-12-34",
+ RhFactor = 1
+ },
+ new
+ {
+ Id = 9,
+ Address = "пр. Невский, д. 45, кв. 19",
+ BirthDate = new DateTime(1970, 12, 3, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 1,
+ FullName = "Зайцев Игорь Павлович",
+ Gender = 1,
+ PassportNumber = "6009 901234",
+ PhoneNumber = "+7 (999) 901-23-45",
+ RhFactor = 0
+ },
+ new
+ {
+ Id = 10,
+ Address = "ул. Комсомольская, д. 6, кв. 23",
+ BirthDate = new DateTime(1965, 4, 20, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 0,
+ FullName = "Волкова Ольга Игоревна",
+ Gender = 2,
+ PassportNumber = "6010 012345",
+ PhoneNumber = "+7 (999) 012-34-56",
+ RhFactor = 0
+ },
+ new
+ {
+ Id = 11,
+ Address = "ул. Мичурина, д. 18, кв. 67",
+ BirthDate = new DateTime(1998, 1, 8, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 2,
+ FullName = "Белова Наталья Сергеевна",
+ Gender = 2,
+ PassportNumber = "6011 123456",
+ PhoneNumber = "+7 (999) 123-56-78",
+ RhFactor = 0
+ },
+ new
+ {
+ Id = 12,
+ Address = "ул. Лермонтова, д. 9, кв. 14",
+ BirthDate = new DateTime(1983, 9, 12, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 3,
+ FullName = "Карпов Евгений Владимирович",
+ Gender = 1,
+ PassportNumber = "6012 234567",
+ PhoneNumber = "+7 (999) 234-67-89",
+ RhFactor = 1
+ });
+ });
+
+ modelBuilder.Entity("Polyclinic.Domain.Entities.Specialization", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Specializations");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ Code = "THERAPIST",
+ Description = "Врач общей практики",
+ Name = "Терапевт"
+ },
+ new
+ {
+ Id = 2,
+ Code = "SURGEON",
+ Description = "Проведение операций",
+ Name = "Хирург"
+ },
+ new
+ {
+ Id = 3,
+ Code = "CARDIOLOGIST",
+ Description = "Заболевания сердца",
+ Name = "Кардиолог"
+ },
+ new
+ {
+ Id = 4,
+ Code = "NEUROLOGIST",
+ Description = "Заболевания нервной системы",
+ Name = "Невролог"
+ },
+ new
+ {
+ Id = 5,
+ Code = "PEDIATRICIAN",
+ Description = "Детские болезни",
+ Name = "Педиатр"
+ },
+ new
+ {
+ Id = 6,
+ Code = "GYNECOLOGIST",
+ Description = "Женское здоровье",
+ Name = "Гинеколог"
+ },
+ new
+ {
+ Id = 7,
+ Code = "OPHTHALMOLOGIST",
+ Description = "Заболевания глаз",
+ Name = "Офтальмолог"
+ },
+ new
+ {
+ Id = 8,
+ Code = "ENT",
+ Description = "Ухо, горло, нос",
+ Name = "Отоларинголог"
+ },
+ new
+ {
+ Id = 9,
+ Code = "DERMATOLOGIST",
+ Description = "Кожные заболевания",
+ Name = "Дерматолог"
+ },
+ new
+ {
+ Id = 10,
+ Code = "ENDOCRINOLOGIST",
+ Description = "Гормональные нарушения",
+ Name = "Эндокринолог"
+ });
+ });
+
+ modelBuilder.Entity("Polyclinic.Domain.Entities.Appointment", b =>
+ {
+ b.HasOne("Polyclinic.Domain.Entities.Doctor", "Doctor")
+ .WithMany("Appointments")
+ .HasForeignKey("DoctorId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("Polyclinic.Domain.Entities.Patient", "Patient")
+ .WithMany("Appointments")
+ .HasForeignKey("PatientId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Doctor");
+
+ b.Navigation("Patient");
+ });
+
+ modelBuilder.Entity("Polyclinic.Domain.Entities.Doctor", b =>
+ {
+ b.HasOne("Polyclinic.Domain.Entities.Specialization", "Specialization")
+ .WithMany("Doctors")
+ .HasForeignKey("SpecializationId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Specialization");
+ });
+
+ modelBuilder.Entity("Polyclinic.Domain.Entities.Doctor", b =>
+ {
+ b.Navigation("Appointments");
+ });
+
+ modelBuilder.Entity("Polyclinic.Domain.Entities.Patient", b =>
+ {
+ b.Navigation("Appointments");
+ });
+
+ modelBuilder.Entity("Polyclinic.Domain.Entities.Specialization", b =>
+ {
+ b.Navigation("Doctors");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/20260216193102_InitialCreate.cs b/Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/20260216193102_InitialCreate.cs
new file mode 100644
index 000000000..e36d11bca
--- /dev/null
+++ b/Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/20260216193102_InitialCreate.cs
@@ -0,0 +1,206 @@
+//
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
+
+namespace Polyclinic.Infrastructure.EfCore.Migrations
+{
+ ///
+ public partial class InitialCreate : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "Patients",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ PassportNumber = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false),
+ FullName = table.Column(type: "nvarchar(150)", maxLength: 150, nullable: false),
+ Gender = table.Column(type: "int", nullable: false),
+ BirthDate = table.Column(type: "datetime2", nullable: false),
+ Address = table.Column(type: "nvarchar(250)", maxLength: 250, nullable: false),
+ BloodGroup = table.Column(type: "int", nullable: false),
+ RhFactor = table.Column(type: "int", nullable: false),
+ PhoneNumber = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Patients", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Specializations",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false),
+ Description = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false),
+ Code = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Specializations", x => x.Id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Doctors",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ PassportNumber = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false),
+ FullName = table.Column(type: "nvarchar(150)", maxLength: 150, nullable: false),
+ BirthDate = table.Column(type: "datetime2", nullable: false),
+ SpecializationId = table.Column(type: "int", nullable: false),
+ ExperienceYears = table.Column(type: "int", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Doctors", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Doctors_Specializations_SpecializationId",
+ column: x => x.SpecializationId,
+ principalTable: "Specializations",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Restrict);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "Appointments",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ AppointmentDateTime = table.Column(type: "datetime2", nullable: false),
+ RoomNumber = table.Column(type: "nvarchar(10)", maxLength: 10, nullable: false),
+ IsRepeat = table.Column(type: "bit", nullable: false),
+ PatientId = table.Column(type: "int", nullable: false),
+ DoctorId = table.Column(type: "int", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Appointments", x => x.Id);
+ table.ForeignKey(
+ name: "FK_Appointments_Doctors_DoctorId",
+ column: x => x.DoctorId,
+ principalTable: "Doctors",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Restrict);
+ table.ForeignKey(
+ name: "FK_Appointments_Patients_PatientId",
+ column: x => x.PatientId,
+ principalTable: "Patients",
+ principalColumn: "Id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.InsertData(
+ table: "Patients",
+ columns: new[] { "Id", "Address", "BirthDate", "BloodGroup", "FullName", "Gender", "PassportNumber", "PhoneNumber", "RhFactor" },
+ values: new object[,]
+ {
+ { 1, "ул. Ленина, д. 10, кв. 25", new DateTime(1990, 5, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), 1, "Смирнов Алексей Викторович", 1, "6001 123456", "+7 (999) 123-45-67", 0 },
+ { 2, "ул. Гагарина, д. 5, кв. 12", new DateTime(1985, 8, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), 0, "Кузнецова Елена Дмитриевна", 2, "6002 234567", "+7 (999) 234-56-78", 0 },
+ { 3, "пр. Мира, д. 15, кв. 7", new DateTime(1978, 3, 10, 0, 0, 0, 0, DateTimeKind.Unspecified), 2, "Попов Сергей Иванович", 1, "6003 345678", "+7 (999) 345-67-89", 1 },
+ { 4, "ул. Советская, д. 8, кв. 42", new DateTime(1995, 11, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), 3, "Васильева Мария Петровна", 2, "6004 456789", "+7 (999) 456-78-90", 0 },
+ { 5, "ул. Пушкина, д. 3, кв. 56", new DateTime(1982, 7, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), 1, "Соколов Андрей Николаевич", 1, "6005 567890", "+7 (999) 567-89-01", 1 },
+ { 6, "пр. Ленинградский, д. 22, кв. 15", new DateTime(1975, 9, 25, 0, 0, 0, 0, DateTimeKind.Unspecified), 0, "Михайлова Анна Сергеевна", 2, "6006 678901", "+7 (999) 678-90-12", 0 },
+ { 7, "ул. Кирова, д. 12, кв. 8", new DateTime(1988, 2, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), 2, "Новиков Денис Александрович", 1, "6007 789012", "+7 (999) 789-01-23", 0 },
+ { 8, "ул. Садовая, д. 7, кв. 31", new DateTime(1992, 6, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), 3, "Морозова Татьяна Владимировна", 2, "6008 890123", "+7 (999) 890-12-34", 1 },
+ { 9, "пр. Невский, д. 45, кв. 19", new DateTime(1970, 12, 3, 0, 0, 0, 0, DateTimeKind.Unspecified), 1, "Зайцев Игорь Павлович", 1, "6009 901234", "+7 (999) 901-23-45", 0 },
+ { 10, "ул. Комсомольская, д. 6, кв. 23", new DateTime(1965, 4, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), 0, "Волкова Ольга Игоревна", 2, "6010 012345", "+7 (999) 012-34-56", 0 },
+ { 11, "ул. Мичурина, д. 18, кв. 67", new DateTime(1998, 1, 8, 0, 0, 0, 0, DateTimeKind.Unspecified), 2, "Белова Наталья Сергеевна", 2, "6011 123456", "+7 (999) 123-56-78", 0 },
+ { 12, "ул. Лермонтова, д. 9, кв. 14", new DateTime(1983, 9, 12, 0, 0, 0, 0, DateTimeKind.Unspecified), 3, "Карпов Евгений Владимирович", 1, "6012 234567", "+7 (999) 234-67-89", 1 }
+ });
+
+ migrationBuilder.InsertData(
+ table: "Specializations",
+ columns: new[] { "Id", "Code", "Description", "Name" },
+ values: new object[,]
+ {
+ { 1, "THERAPIST", "Врач общей практики", "Терапевт" },
+ { 2, "SURGEON", "Проведение операций", "Хирург" },
+ { 3, "CARDIOLOGIST", "Заболевания сердца", "Кардиолог" },
+ { 4, "NEUROLOGIST", "Заболевания нервной системы", "Невролог" },
+ { 5, "PEDIATRICIAN", "Детские болезни", "Педиатр" },
+ { 6, "GYNECOLOGIST", "Женское здоровье", "Гинеколог" },
+ { 7, "OPHTHALMOLOGIST", "Заболевания глаз", "Офтальмолог" },
+ { 8, "ENT", "Ухо, горло, нос", "Отоларинголог" },
+ { 9, "DERMATOLOGIST", "Кожные заболевания", "Дерматолог" },
+ { 10, "ENDOCRINOLOGIST", "Гормональные нарушения", "Эндокринолог" }
+ });
+
+ migrationBuilder.InsertData(
+ table: "Doctors",
+ columns: new[] { "Id", "BirthDate", "ExperienceYears", "FullName", "PassportNumber", "SpecializationId" },
+ values: new object[,]
+ {
+ { 1, new DateTime(1980, 5, 15, 0, 0, 0, 0, DateTimeKind.Unspecified), 15, "Иванов Иван Иванович", "4501 123456", 1 },
+ { 2, new DateTime(1975, 8, 22, 0, 0, 0, 0, DateTimeKind.Unspecified), 20, "Петров Петр Петрович", "4502 234567", 2 },
+ { 3, new DateTime(1985, 3, 10, 0, 0, 0, 0, DateTimeKind.Unspecified), 12, "Сидорова Анна Сергеевна", "4503 345678", 3 },
+ { 4, new DateTime(1990, 11, 30, 0, 0, 0, 0, DateTimeKind.Unspecified), 8, "Козлов Дмитрий Николаевич", "4504 456789", 4 },
+ { 5, new DateTime(1982, 7, 18, 0, 0, 0, 0, DateTimeKind.Unspecified), 14, "Морозова Елена Владимировна", "4505 567890", 5 },
+ { 6, new DateTime(1978, 9, 25, 0, 0, 0, 0, DateTimeKind.Unspecified), 18, "Волков Андрей Игоревич", "4506 678901", 6 },
+ { 7, new DateTime(1988, 2, 14, 0, 0, 0, 0, DateTimeKind.Unspecified), 10, "Соколова Татьяна Александровна", "4507 789012", 7 },
+ { 8, new DateTime(1992, 6, 5, 0, 0, 0, 0, DateTimeKind.Unspecified), 6, "Лебедев Михаил Сергеевич", "4508 890123", 8 },
+ { 9, new DateTime(1983, 12, 3, 0, 0, 0, 0, DateTimeKind.Unspecified), 13, "Николаева Ольга Викторовна", "4509 901234", 9 },
+ { 10, new DateTime(1970, 4, 20, 0, 0, 0, 0, DateTimeKind.Unspecified), 25, "Федоров Алексей Павлович", "4510 012345", 10 }
+ });
+
+ migrationBuilder.InsertData(
+ table: "Appointments",
+ columns: new[] { "Id", "AppointmentDateTime", "DoctorId", "IsRepeat", "PatientId", "RoomNumber" },
+ values: new object[,]
+ {
+ { 1, new DateTime(2026, 2, 5, 10, 0, 0, 0, DateTimeKind.Unspecified), 1, false, 1, "101" },
+ { 2, new DateTime(2026, 2, 5, 11, 0, 0, 0, DateTimeKind.Unspecified), 1, true, 2, "101" },
+ { 3, new DateTime(2026, 2, 10, 14, 30, 0, 0, DateTimeKind.Unspecified), 2, false, 3, "202" },
+ { 4, new DateTime(2026, 2, 15, 9, 15, 0, 0, DateTimeKind.Unspecified), 3, false, 4, "303" },
+ { 5, new DateTime(2026, 2, 12, 13, 30, 0, 0, DateTimeKind.Unspecified), 1, false, 8, "101" },
+ { 6, new DateTime(2026, 2, 16, 9, 30, 0, 0, DateTimeKind.Unspecified), 1, false, 11, "101" },
+ { 7, new DateTime(2026, 1, 15, 10, 0, 0, 0, DateTimeKind.Unspecified), 2, true, 6, "101" },
+ { 8, new DateTime(2026, 1, 20, 11, 0, 0, 0, DateTimeKind.Unspecified), 2, true, 7, "202" },
+ { 9, new DateTime(2026, 1, 5, 9, 0, 0, 0, DateTimeKind.Unspecified), 3, false, 8, "303" },
+ { 10, new DateTime(2026, 2, 8, 9, 0, 0, 0, DateTimeKind.Unspecified), 2, false, 5, "102" },
+ { 11, new DateTime(2026, 2, 22, 11, 30, 0, 0, DateTimeKind.Unspecified), 6, false, 5, "606" }
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Appointments_DoctorId",
+ table: "Appointments",
+ column: "DoctorId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Appointments_PatientId",
+ table: "Appointments",
+ column: "PatientId");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_Doctors_SpecializationId",
+ table: "Doctors",
+ column: "SpecializationId");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "Appointments");
+
+ migrationBuilder.DropTable(
+ name: "Doctors");
+
+ migrationBuilder.DropTable(
+ name: "Patients");
+
+ migrationBuilder.DropTable(
+ name: "Specializations");
+ }
+ }
+}
diff --git a/Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/PolyclinicDbContextModelSnapshot.cs b/Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/PolyclinicDbContextModelSnapshot.cs
new file mode 100644
index 000000000..3b052e867
--- /dev/null
+++ b/Polyclinic/Polyclinic.Infrastructure.EfCore/Migrations/PolyclinicDbContextModelSnapshot.cs
@@ -0,0 +1,624 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Polyclinic.Infrastructure.EfCore;
+
+#nullable disable
+
+namespace Polyclinic.Infrastructure.EfCore.Migrations
+{
+ [DbContext(typeof(PolyclinicDbContext))]
+ partial class PolyclinicDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.13")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("Polyclinic.Domain.Entities.Appointment", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("AppointmentDateTime")
+ .HasColumnType("datetime2");
+
+ b.Property("DoctorId")
+ .HasColumnType("int");
+
+ b.Property("IsRepeat")
+ .HasColumnType("bit");
+
+ b.Property("PatientId")
+ .HasColumnType("int");
+
+ b.Property("RoomNumber")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("nvarchar(10)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DoctorId");
+
+ b.HasIndex("PatientId");
+
+ b.ToTable("Appointments");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ AppointmentDateTime = new DateTime(2026, 2, 5, 10, 0, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 1,
+ IsRepeat = false,
+ PatientId = 1,
+ RoomNumber = "101"
+ },
+ new
+ {
+ Id = 2,
+ AppointmentDateTime = new DateTime(2026, 2, 5, 11, 0, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 1,
+ IsRepeat = true,
+ PatientId = 2,
+ RoomNumber = "101"
+ },
+ new
+ {
+ Id = 3,
+ AppointmentDateTime = new DateTime(2026, 2, 10, 14, 30, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 2,
+ IsRepeat = false,
+ PatientId = 3,
+ RoomNumber = "202"
+ },
+ new
+ {
+ Id = 4,
+ AppointmentDateTime = new DateTime(2026, 2, 15, 9, 15, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 3,
+ IsRepeat = false,
+ PatientId = 4,
+ RoomNumber = "303"
+ },
+ new
+ {
+ Id = 5,
+ AppointmentDateTime = new DateTime(2026, 2, 12, 13, 30, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 1,
+ IsRepeat = false,
+ PatientId = 8,
+ RoomNumber = "101"
+ },
+ new
+ {
+ Id = 6,
+ AppointmentDateTime = new DateTime(2026, 2, 16, 9, 30, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 1,
+ IsRepeat = false,
+ PatientId = 11,
+ RoomNumber = "101"
+ },
+ new
+ {
+ Id = 7,
+ AppointmentDateTime = new DateTime(2026, 1, 15, 10, 0, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 2,
+ IsRepeat = true,
+ PatientId = 6,
+ RoomNumber = "101"
+ },
+ new
+ {
+ Id = 8,
+ AppointmentDateTime = new DateTime(2026, 1, 20, 11, 0, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 2,
+ IsRepeat = true,
+ PatientId = 7,
+ RoomNumber = "202"
+ },
+ new
+ {
+ Id = 9,
+ AppointmentDateTime = new DateTime(2026, 1, 5, 9, 0, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 3,
+ IsRepeat = false,
+ PatientId = 8,
+ RoomNumber = "303"
+ },
+ new
+ {
+ Id = 10,
+ AppointmentDateTime = new DateTime(2026, 2, 8, 9, 0, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 2,
+ IsRepeat = false,
+ PatientId = 5,
+ RoomNumber = "102"
+ },
+ new
+ {
+ Id = 11,
+ AppointmentDateTime = new DateTime(2026, 2, 22, 11, 30, 0, 0, DateTimeKind.Unspecified),
+ DoctorId = 6,
+ IsRepeat = false,
+ PatientId = 5,
+ RoomNumber = "606"
+ });
+ });
+
+ modelBuilder.Entity("Polyclinic.Domain.Entities.Doctor", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("BirthDate")
+ .HasColumnType("datetime2");
+
+ b.Property("ExperienceYears")
+ .HasColumnType("int");
+
+ b.Property("FullName")
+ .IsRequired()
+ .HasMaxLength(150)
+ .HasColumnType("nvarchar(150)");
+
+ b.Property("PassportNumber")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.Property("SpecializationId")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SpecializationId");
+
+ b.ToTable("Doctors");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ BirthDate = new DateTime(1980, 5, 15, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ ExperienceYears = 15,
+ FullName = "Иванов Иван Иванович",
+ PassportNumber = "4501 123456",
+ SpecializationId = 1
+ },
+ new
+ {
+ Id = 2,
+ BirthDate = new DateTime(1975, 8, 22, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ ExperienceYears = 20,
+ FullName = "Петров Петр Петрович",
+ PassportNumber = "4502 234567",
+ SpecializationId = 2
+ },
+ new
+ {
+ Id = 3,
+ BirthDate = new DateTime(1985, 3, 10, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ ExperienceYears = 12,
+ FullName = "Сидорова Анна Сергеевна",
+ PassportNumber = "4503 345678",
+ SpecializationId = 3
+ },
+ new
+ {
+ Id = 4,
+ BirthDate = new DateTime(1990, 11, 30, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ ExperienceYears = 8,
+ FullName = "Козлов Дмитрий Николаевич",
+ PassportNumber = "4504 456789",
+ SpecializationId = 4
+ },
+ new
+ {
+ Id = 5,
+ BirthDate = new DateTime(1982, 7, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ ExperienceYears = 14,
+ FullName = "Морозова Елена Владимировна",
+ PassportNumber = "4505 567890",
+ SpecializationId = 5
+ },
+ new
+ {
+ Id = 6,
+ BirthDate = new DateTime(1978, 9, 25, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ ExperienceYears = 18,
+ FullName = "Волков Андрей Игоревич",
+ PassportNumber = "4506 678901",
+ SpecializationId = 6
+ },
+ new
+ {
+ Id = 7,
+ BirthDate = new DateTime(1988, 2, 14, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ ExperienceYears = 10,
+ FullName = "Соколова Татьяна Александровна",
+ PassportNumber = "4507 789012",
+ SpecializationId = 7
+ },
+ new
+ {
+ Id = 8,
+ BirthDate = new DateTime(1992, 6, 5, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ ExperienceYears = 6,
+ FullName = "Лебедев Михаил Сергеевич",
+ PassportNumber = "4508 890123",
+ SpecializationId = 8
+ },
+ new
+ {
+ Id = 9,
+ BirthDate = new DateTime(1983, 12, 3, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ ExperienceYears = 13,
+ FullName = "Николаева Ольга Викторовна",
+ PassportNumber = "4509 901234",
+ SpecializationId = 9
+ },
+ new
+ {
+ Id = 10,
+ BirthDate = new DateTime(1970, 4, 20, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ ExperienceYears = 25,
+ FullName = "Федоров Алексей Павлович",
+ PassportNumber = "4510 012345",
+ SpecializationId = 10
+ });
+ });
+
+ modelBuilder.Entity("Polyclinic.Domain.Entities.Patient", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .IsRequired()
+ .HasMaxLength(250)
+ .HasColumnType("nvarchar(250)");
+
+ b.Property("BirthDate")
+ .HasColumnType("datetime2");
+
+ b.Property("BloodGroup")
+ .HasColumnType("int");
+
+ b.Property("FullName")
+ .IsRequired()
+ .HasMaxLength(150)
+ .HasColumnType("nvarchar(150)");
+
+ b.Property("Gender")
+ .HasColumnType("int");
+
+ b.Property("PassportNumber")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.Property("PhoneNumber")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.Property("RhFactor")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.ToTable("Patients");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ Address = "ул. Ленина, д. 10, кв. 25",
+ BirthDate = new DateTime(1990, 5, 15, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 1,
+ FullName = "Смирнов Алексей Викторович",
+ Gender = 1,
+ PassportNumber = "6001 123456",
+ PhoneNumber = "+7 (999) 123-45-67",
+ RhFactor = 0
+ },
+ new
+ {
+ Id = 2,
+ Address = "ул. Гагарина, д. 5, кв. 12",
+ BirthDate = new DateTime(1985, 8, 22, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 0,
+ FullName = "Кузнецова Елена Дмитриевна",
+ Gender = 2,
+ PassportNumber = "6002 234567",
+ PhoneNumber = "+7 (999) 234-56-78",
+ RhFactor = 0
+ },
+ new
+ {
+ Id = 3,
+ Address = "пр. Мира, д. 15, кв. 7",
+ BirthDate = new DateTime(1978, 3, 10, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 2,
+ FullName = "Попов Сергей Иванович",
+ Gender = 1,
+ PassportNumber = "6003 345678",
+ PhoneNumber = "+7 (999) 345-67-89",
+ RhFactor = 1
+ },
+ new
+ {
+ Id = 4,
+ Address = "ул. Советская, д. 8, кв. 42",
+ BirthDate = new DateTime(1995, 11, 30, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 3,
+ FullName = "Васильева Мария Петровна",
+ Gender = 2,
+ PassportNumber = "6004 456789",
+ PhoneNumber = "+7 (999) 456-78-90",
+ RhFactor = 0
+ },
+ new
+ {
+ Id = 5,
+ Address = "ул. Пушкина, д. 3, кв. 56",
+ BirthDate = new DateTime(1982, 7, 18, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 1,
+ FullName = "Соколов Андрей Николаевич",
+ Gender = 1,
+ PassportNumber = "6005 567890",
+ PhoneNumber = "+7 (999) 567-89-01",
+ RhFactor = 1
+ },
+ new
+ {
+ Id = 6,
+ Address = "пр. Ленинградский, д. 22, кв. 15",
+ BirthDate = new DateTime(1975, 9, 25, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 0,
+ FullName = "Михайлова Анна Сергеевна",
+ Gender = 2,
+ PassportNumber = "6006 678901",
+ PhoneNumber = "+7 (999) 678-90-12",
+ RhFactor = 0
+ },
+ new
+ {
+ Id = 7,
+ Address = "ул. Кирова, д. 12, кв. 8",
+ BirthDate = new DateTime(1988, 2, 14, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 2,
+ FullName = "Новиков Денис Александрович",
+ Gender = 1,
+ PassportNumber = "6007 789012",
+ PhoneNumber = "+7 (999) 789-01-23",
+ RhFactor = 0
+ },
+ new
+ {
+ Id = 8,
+ Address = "ул. Садовая, д. 7, кв. 31",
+ BirthDate = new DateTime(1992, 6, 5, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 3,
+ FullName = "Морозова Татьяна Владимировна",
+ Gender = 2,
+ PassportNumber = "6008 890123",
+ PhoneNumber = "+7 (999) 890-12-34",
+ RhFactor = 1
+ },
+ new
+ {
+ Id = 9,
+ Address = "пр. Невский, д. 45, кв. 19",
+ BirthDate = new DateTime(1970, 12, 3, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 1,
+ FullName = "Зайцев Игорь Павлович",
+ Gender = 1,
+ PassportNumber = "6009 901234",
+ PhoneNumber = "+7 (999) 901-23-45",
+ RhFactor = 0
+ },
+ new
+ {
+ Id = 10,
+ Address = "ул. Комсомольская, д. 6, кв. 23",
+ BirthDate = new DateTime(1965, 4, 20, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 0,
+ FullName = "Волкова Ольга Игоревна",
+ Gender = 2,
+ PassportNumber = "6010 012345",
+ PhoneNumber = "+7 (999) 012-34-56",
+ RhFactor = 0
+ },
+ new
+ {
+ Id = 11,
+ Address = "ул. Мичурина, д. 18, кв. 67",
+ BirthDate = new DateTime(1998, 1, 8, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 2,
+ FullName = "Белова Наталья Сергеевна",
+ Gender = 2,
+ PassportNumber = "6011 123456",
+ PhoneNumber = "+7 (999) 123-56-78",
+ RhFactor = 0
+ },
+ new
+ {
+ Id = 12,
+ Address = "ул. Лермонтова, д. 9, кв. 14",
+ BirthDate = new DateTime(1983, 9, 12, 0, 0, 0, 0, DateTimeKind.Unspecified),
+ BloodGroup = 3,
+ FullName = "Карпов Евгений Владимирович",
+ Gender = 1,
+ PassportNumber = "6012 234567",
+ PhoneNumber = "+7 (999) 234-67-89",
+ RhFactor = 1
+ });
+ });
+
+ modelBuilder.Entity("Polyclinic.Domain.Entities.Specialization", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("nvarchar(20)");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Specializations");
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ Code = "THERAPIST",
+ Description = "Врач общей практики",
+ Name = "Терапевт"
+ },
+ new
+ {
+ Id = 2,
+ Code = "SURGEON",
+ Description = "Проведение операций",
+ Name = "Хирург"
+ },
+ new
+ {
+ Id = 3,
+ Code = "CARDIOLOGIST",
+ Description = "Заболевания сердца",
+ Name = "Кардиолог"
+ },
+ new
+ {
+ Id = 4,
+ Code = "NEUROLOGIST",
+ Description = "Заболевания нервной системы",
+ Name = "Невролог"
+ },
+ new
+ {
+ Id = 5,
+ Code = "PEDIATRICIAN",
+ Description = "Детские болезни",
+ Name = "Педиатр"
+ },
+ new
+ {
+ Id = 6,
+ Code = "GYNECOLOGIST",
+ Description = "Женское здоровье",
+ Name = "Гинеколог"
+ },
+ new
+ {
+ Id = 7,
+ Code = "OPHTHALMOLOGIST",
+ Description = "Заболевания глаз",
+ Name = "Офтальмолог"
+ },
+ new
+ {
+ Id = 8,
+ Code = "ENT",
+ Description = "Ухо, горло, нос",
+ Name = "Отоларинголог"
+ },
+ new
+ {
+ Id = 9,
+ Code = "DERMATOLOGIST",
+ Description = "Кожные заболевания",
+ Name = "Дерматолог"
+ },
+ new
+ {
+ Id = 10,
+ Code = "ENDOCRINOLOGIST",
+ Description = "Гормональные нарушения",
+ Name = "Эндокринолог"
+ });
+ });
+
+ modelBuilder.Entity("Polyclinic.Domain.Entities.Appointment", b =>
+ {
+ b.HasOne("Polyclinic.Domain.Entities.Doctor", "Doctor")
+ .WithMany("Appointments")
+ .HasForeignKey("DoctorId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("Polyclinic.Domain.Entities.Patient", "Patient")
+ .WithMany("Appointments")
+ .HasForeignKey("PatientId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Doctor");
+
+ b.Navigation("Patient");
+ });
+
+ modelBuilder.Entity("Polyclinic.Domain.Entities.Doctor", b =>
+ {
+ b.HasOne("Polyclinic.Domain.Entities.Specialization", "Specialization")
+ .WithMany("Doctors")
+ .HasForeignKey("SpecializationId")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.Navigation("Specialization");
+ });
+
+ modelBuilder.Entity("Polyclinic.Domain.Entities.Doctor", b =>
+ {
+ b.Navigation("Appointments");
+ });
+
+ modelBuilder.Entity("Polyclinic.Domain.Entities.Patient", b =>
+ {
+ b.Navigation("Appointments");
+ });
+
+ modelBuilder.Entity("Polyclinic.Domain.Entities.Specialization", b =>
+ {
+ b.Navigation("Doctors");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Polyclinic/Polyclinic.Infrastructure.EfCore/Polyclinic.Infrastructure.EfCore.csproj b/Polyclinic/Polyclinic.Infrastructure.EfCore/Polyclinic.Infrastructure.EfCore.csproj
new file mode 100644
index 000000000..edff78024
--- /dev/null
+++ b/Polyclinic/Polyclinic.Infrastructure.EfCore/Polyclinic.Infrastructure.EfCore.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Polyclinic/Polyclinic.Infrastructure.EfCore/PolyclinicDbContext.cs b/Polyclinic/Polyclinic.Infrastructure.EfCore/PolyclinicDbContext.cs
new file mode 100644
index 000000000..6965e1b19
--- /dev/null
+++ b/Polyclinic/Polyclinic.Infrastructure.EfCore/PolyclinicDbContext.cs
@@ -0,0 +1,165 @@
+using Microsoft.EntityFrameworkCore;
+using Polyclinic.Domain;
+using Polyclinic.Domain.Entities;
+
+namespace Polyclinic.Infrastructure.EfCore;
+
+///
+/// Контекст базы данных поликлиники
+///
+public class PolyclinicDbContext(
+ DbContextOptions options,
+ PolyclinicFixture fixture) : DbContext(options)
+{
+ ///
+ /// Таблица специализаций врачей (справочник)
+ ///
+ public DbSet Specializations { get; set; }
+
+ ///
+ /// Таблица врачей поликлиники
+ ///
+ public DbSet Doctors { get; set; }
+
+ ///
+ /// Таблица пациентов
+ ///
+ public DbSet Patients { get; set; }
+
+ ///
+ /// Таблица записей на прием (журнал посещений)
+ ///
+ public DbSet Appointments { get; set; }
+
+ ///
+ /// Настройка модели базы данных при её создании
+ ///
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ base.OnModelCreating(modelBuilder);
+
+ ConfigureSpecialization(modelBuilder);
+ ConfigureDoctor(modelBuilder);
+ ConfigurePatient(modelBuilder);
+ ConfigureAppointment(modelBuilder);
+ }
+
+ ///
+ /// Конфигурация сущности
+ ///
+ private void ConfigureSpecialization(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.Id);
+
+ entity.Property(e => e.Name)
+ .IsRequired()
+ .HasMaxLength(100);
+
+ entity.Property(e => e.Code)
+ .IsRequired()
+ .HasMaxLength(20);
+
+ entity.Property(e => e.Description)
+ .HasMaxLength(500);
+
+ if (fixture.Specializations.Count > 0)
+ {
+ entity.HasData(fixture.Specializations);
+ }
+ });
+ }
+
+ ///
+ /// Конфигурация сущности
+ ///
+ private void ConfigureDoctor(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.Id);
+
+ entity.Property(e => e.FullName)
+ .IsRequired()
+ .HasMaxLength(150);
+
+ entity.Property(e => e.PassportNumber)
+ .IsRequired()
+ .HasMaxLength(20);
+
+ entity.HasOne(d => d.Specialization)
+ .WithMany(s => s.Doctors)
+ .HasForeignKey(d => d.SpecializationId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ if (fixture.Doctors.Count > 0)
+ {
+ entity.HasData(fixture.Doctors);
+ }
+ });
+ }
+
+ ///
+ /// Конфигурация сущности
+ ///
+ private void ConfigurePatient(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.Id);
+
+ entity.Property(e => e.FullName)
+ .IsRequired()
+ .HasMaxLength(150);
+
+ entity.Property(e => e.PassportNumber)
+ .IsRequired()
+ .HasMaxLength(20);
+
+ entity.Property(e => e.Address)
+ .HasMaxLength(250);
+
+ entity.Property(e => e.PhoneNumber)
+ .HasMaxLength(20);
+
+ if (fixture.Patients.Count > 0)
+ {
+ entity.HasData(fixture.Patients);
+ }
+ });
+ }
+
+ ///
+ /// Конфигурация сущности
+ ///
+ private void ConfigureAppointment(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity(entity =>
+ {
+ entity.HasKey(e => e.Id);
+
+ entity.Property(e => e.RoomNumber)
+ .HasMaxLength(10)
+ .IsRequired();
+
+ entity.Property(e => e.AppointmentDateTime)
+ .HasColumnType("datetime2");
+
+ entity.HasOne(a => a.Patient)
+ .WithMany(p => p.Appointments)
+ .HasForeignKey(a => a.PatientId)
+ .OnDelete(DeleteBehavior.Cascade);
+
+ entity.HasOne(a => a.Doctor)
+ .WithMany(d => d.Appointments)
+ .HasForeignKey(a => a.DoctorId)
+ .OnDelete(DeleteBehavior.Restrict);
+
+ if (fixture.Appointments.Count > 0)
+ {
+ entity.HasData(fixture.Appointments);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/AppointmentRepository.cs b/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/AppointmentRepository.cs
new file mode 100644
index 000000000..2396d127b
--- /dev/null
+++ b/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/AppointmentRepository.cs
@@ -0,0 +1,71 @@
+using Microsoft.EntityFrameworkCore;
+using Polyclinic.Domain;
+using Polyclinic.Domain.Entities;
+
+namespace Polyclinic.Infrastructure.EfCore.Repositories;
+
+///
+/// Репозиторий для управления записями на прием
+///
+public class AppointmentRepository(PolyclinicDbContext context) : IRepository
+{
+ ///
+ /// Создаёт новую запись на прием в базе данных
+ ///
+ public async Task Create(Appointment entity)
+ {
+ await context.Appointments.AddAsync(entity);
+ await context.SaveChangesAsync();
+ return entity;
+ }
+
+ ///
+ /// Получает запись по идентификатору с данными о враче и пациенте
+ ///
+ public async Task Read(int entityId)
+ {
+ return await context.Appointments
+ .Include(a => a.Patient)
+ .Include(a => a.Doctor)
+ .ThenInclude(d => d!.Specialization)
+ .FirstOrDefaultAsync(a => a.Id == entityId);
+ }
+
+ ///
+ /// Получает список всех записей с данными о врачах и пациентах
+ ///
+ public async Task> ReadAll()
+ {
+ return await context.Appointments
+ .Include(a => a.Patient)
+ .Include(a => a.Doctor)
+ .ThenInclude(d => d!.Specialization)
+ .ToListAsync();
+ }
+
+ ///
+ /// Обновляет данные записи на прием
+ ///
+ public async Task Update(Appointment entity)
+ {
+ context.Appointments.Update(entity);
+ await context.SaveChangesAsync();
+ return entity;
+ }
+
+ ///
+ /// Удаляет запись на прием по идентификатору
+ ///
+ public async Task Delete(int entityId)
+ {
+ var entity = await context.Appointments.FirstOrDefaultAsync(e => e.Id == entityId);
+ if (entity == null)
+ {
+ return false;
+ }
+
+ context.Appointments.Remove(entity);
+ await context.SaveChangesAsync();
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/DoctorRepository.cs b/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/DoctorRepository.cs
new file mode 100644
index 000000000..b51313ca6
--- /dev/null
+++ b/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/DoctorRepository.cs
@@ -0,0 +1,67 @@
+using Microsoft.EntityFrameworkCore;
+using Polyclinic.Domain;
+using Polyclinic.Domain.Entities;
+
+namespace Polyclinic.Infrastructure.EfCore.Repositories;
+
+///
+/// Репозиторий для управления врачами
+///
+public class DoctorRepository(PolyclinicDbContext context) : IRepository
+{
+ ///
+ /// Создаёт нового врача в базе данных
+ ///
+ public async Task Create(Doctor entity)
+ {
+ await context.Doctors.AddAsync(entity);
+ await context.SaveChangesAsync();
+ return entity;
+ }
+
+ ///
+ /// Получает врача по идентификатору вместе со специализацией
+ ///
+ public async Task Read(int entityId)
+ {
+ return await context.Doctors
+ .Include(d => d.Specialization)
+ .FirstOrDefaultAsync(d => d.Id == entityId);
+ }
+
+ ///
+ /// Получает список всех врачей вместе с их специализациями
+ ///
+ public async Task> ReadAll()
+ {
+ return await context.Doctors
+ .Include(d => d.Specialization)
+ .ToListAsync();
+ }
+
+ ///
+ /// Обновляет данные врача
+ ///
+ public async Task Update(Doctor entity)
+ {
+ context.Doctors.Update(entity);
+ await context.SaveChangesAsync();
+ return entity;
+ }
+
+ ///
+ /// Удаляет врача по идентификатору
+ ///
+ public async Task Delete(int entityId)
+ {
+ var entity = await context.Doctors.FirstOrDefaultAsync(e => e.Id == entityId);
+ if (entity == null)
+ {
+ return false;
+ }
+
+ context.Doctors.Remove(entity);
+ await context.SaveChangesAsync();
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/PatientRepository.cs b/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/PatientRepository.cs
new file mode 100644
index 000000000..e68717bac
--- /dev/null
+++ b/Polyclinic/Polyclinic.Infrastructure.EfCore/Repositories/PatientRepository.cs
@@ -0,0 +1,63 @@
+using Microsoft.EntityFrameworkCore;
+using Polyclinic.Domain;
+using Polyclinic.Domain.Entities;
+
+namespace Polyclinic.Infrastructure.EfCore.Repositories;
+
+///
+/// Репозиторий для управления пациентами
+///
+public class PatientRepository(PolyclinicDbContext context) : IRepository
+{
+ ///
+ /// Создаёт нового пациента в базе данных
+ ///
+ public async Task Create(Patient entity)
+ {
+ await context.Patients.AddAsync(entity);
+ await context.SaveChangesAsync();
+ return entity;
+ }
+
+ ///
+ /// Получает пациента по идентификатору
+ ///
+ public async Task Read(int entityId)
+ {
+ return await context.Patients.FirstOrDefaultAsync(e => e.Id == entityId);
+ }
+
+ ///