diff --git a/.gitignore b/.gitignore
index ce892922..0db959ea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -416,3 +416,5 @@ FodyWeavers.xsd
*.msix
*.msm
*.msp
+
+**/appsettings.Development.json
\ No newline at end of file
diff --git a/Client.Wasm/Components/StudentCard.razor b/Client.Wasm/Components/StudentCard.razor
index 661f1181..b6074751 100644
--- a/Client.Wasm/Components/StudentCard.razor
+++ b/Client.Wasm/Components/StudentCard.razor
@@ -4,10 +4,10 @@
- Номер №X "Название лабораторной"
- Вариант №Х "Название варианта"
- Выполнена Фамилией Именем 65ХХ
- Ссылка на форк
+ Номер №3 "Интеграционное тестирование"
+ Вариант №53 "Кредитная заявка"
+ Выполнена Уваровым Никитой 6513
+ Ссылка на форк
diff --git a/Client.Wasm/wwwroot/appsettings.json b/Client.Wasm/wwwroot/appsettings.json
index d1fe7ab3..e9e4c9f3 100644
--- a/Client.Wasm/wwwroot/appsettings.json
+++ b/Client.Wasm/wwwroot/appsettings.json
@@ -6,5 +6,5 @@
}
},
"AllowedHosts": "*",
- "BaseAddress": ""
+ "BaseAddress": "https://localhost:9002/api/Credit"
}
diff --git a/CloudDevelopment.sln b/CloudDevelopment.sln
index cb48241d..cb646920 100644
--- a/CloudDevelopment.sln
+++ b/CloudDevelopment.sln
@@ -1,20 +1,144 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.14.36811.4
+# Visual Studio Version 18
+VisualStudioVersion = 18.3.11512.155
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Wasm", "Client.Wasm\Client.Wasm.csproj", "{AE7EEA74-2FE0-136F-D797-854FD87E022A}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreditApp.AppHost", "CreditApp.AppHost\CreditApp.AppHost.csproj", "{5432516B-65B7-417A-9D7C-D87F95B880D5}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreditApp.ServiceDefaults", "CreditApp.ServiceDefaults\CreditApp.ServiceDefaults.csproj", "{2A1134C7-1080-475D-2A48-9F65479D8C91}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreditApp.Api", "CreditApp.Api\CreditApp.Api.csproj", "{E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreditApp.Domain", "CreditApp.Domain\CreditApp.Domain.csproj", "{B3DC2A03-88A2-468A-BE14-95A1F4DEA883}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreditApp.Gateway", "CreditApp.Gateway\CreditApp.Gateway.csproj", "{3BA8A38C-6AFF-C134-67A1-BC28FDD7ACF1}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreditApp.Messaging", "CreditApp.Messaging\CreditApp.Messaging.csproj", "{6D9BB2AD-FAB4-4C5C-97FC-A20AFD3AA13A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreditApp.FileService", "CreditApp.FileService\CreditApp.FileService.csproj", "{9EA9ED81-535D-44CA-8322-D4FB841E17CB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CreditApp.Tests", "CreditApp.Tests\CreditApp.Tests.csproj", "{1C8C5E5F-8C5C-4F3F-B526-C9F241E598F1}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|x64.Build.0 = Debug|Any CPU
+ {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Debug|x86.Build.0 = Debug|Any CPU
{AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|x64.ActiveCfg = Release|Any CPU
+ {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|x64.Build.0 = Release|Any CPU
+ {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|x86.ActiveCfg = Release|Any CPU
+ {AE7EEA74-2FE0-136F-D797-854FD87E022A}.Release|x86.Build.0 = Release|Any CPU
+ {5432516B-65B7-417A-9D7C-D87F95B880D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5432516B-65B7-417A-9D7C-D87F95B880D5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5432516B-65B7-417A-9D7C-D87F95B880D5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5432516B-65B7-417A-9D7C-D87F95B880D5}.Debug|x64.Build.0 = Debug|Any CPU
+ {5432516B-65B7-417A-9D7C-D87F95B880D5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5432516B-65B7-417A-9D7C-D87F95B880D5}.Debug|x86.Build.0 = Debug|Any CPU
+ {5432516B-65B7-417A-9D7C-D87F95B880D5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5432516B-65B7-417A-9D7C-D87F95B880D5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5432516B-65B7-417A-9D7C-D87F95B880D5}.Release|x64.ActiveCfg = Release|Any CPU
+ {5432516B-65B7-417A-9D7C-D87F95B880D5}.Release|x64.Build.0 = Release|Any CPU
+ {5432516B-65B7-417A-9D7C-D87F95B880D5}.Release|x86.ActiveCfg = Release|Any CPU
+ {5432516B-65B7-417A-9D7C-D87F95B880D5}.Release|x86.Build.0 = Release|Any CPU
+ {2A1134C7-1080-475D-2A48-9F65479D8C91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2A1134C7-1080-475D-2A48-9F65479D8C91}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2A1134C7-1080-475D-2A48-9F65479D8C91}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {2A1134C7-1080-475D-2A48-9F65479D8C91}.Debug|x64.Build.0 = Debug|Any CPU
+ {2A1134C7-1080-475D-2A48-9F65479D8C91}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {2A1134C7-1080-475D-2A48-9F65479D8C91}.Debug|x86.Build.0 = Debug|Any CPU
+ {2A1134C7-1080-475D-2A48-9F65479D8C91}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2A1134C7-1080-475D-2A48-9F65479D8C91}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2A1134C7-1080-475D-2A48-9F65479D8C91}.Release|x64.ActiveCfg = Release|Any CPU
+ {2A1134C7-1080-475D-2A48-9F65479D8C91}.Release|x64.Build.0 = Release|Any CPU
+ {2A1134C7-1080-475D-2A48-9F65479D8C91}.Release|x86.ActiveCfg = Release|Any CPU
+ {2A1134C7-1080-475D-2A48-9F65479D8C91}.Release|x86.Build.0 = Release|Any CPU
+ {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Debug|x64.Build.0 = Debug|Any CPU
+ {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Debug|x86.Build.0 = Debug|Any CPU
+ {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Release|x64.ActiveCfg = Release|Any CPU
+ {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Release|x64.Build.0 = Release|Any CPU
+ {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Release|x86.ActiveCfg = Release|Any CPU
+ {E7D4CA8B-53EA-9676-D96D-BE2F0CB11054}.Release|x86.Build.0 = Release|Any CPU
+ {B3DC2A03-88A2-468A-BE14-95A1F4DEA883}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B3DC2A03-88A2-468A-BE14-95A1F4DEA883}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B3DC2A03-88A2-468A-BE14-95A1F4DEA883}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {B3DC2A03-88A2-468A-BE14-95A1F4DEA883}.Debug|x64.Build.0 = Debug|Any CPU
+ {B3DC2A03-88A2-468A-BE14-95A1F4DEA883}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {B3DC2A03-88A2-468A-BE14-95A1F4DEA883}.Debug|x86.Build.0 = Debug|Any CPU
+ {B3DC2A03-88A2-468A-BE14-95A1F4DEA883}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B3DC2A03-88A2-468A-BE14-95A1F4DEA883}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B3DC2A03-88A2-468A-BE14-95A1F4DEA883}.Release|x64.ActiveCfg = Release|Any CPU
+ {B3DC2A03-88A2-468A-BE14-95A1F4DEA883}.Release|x64.Build.0 = Release|Any CPU
+ {B3DC2A03-88A2-468A-BE14-95A1F4DEA883}.Release|x86.ActiveCfg = Release|Any CPU
+ {B3DC2A03-88A2-468A-BE14-95A1F4DEA883}.Release|x86.Build.0 = Release|Any CPU
+ {3BA8A38C-6AFF-C134-67A1-BC28FDD7ACF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3BA8A38C-6AFF-C134-67A1-BC28FDD7ACF1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3BA8A38C-6AFF-C134-67A1-BC28FDD7ACF1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {3BA8A38C-6AFF-C134-67A1-BC28FDD7ACF1}.Debug|x64.Build.0 = Debug|Any CPU
+ {3BA8A38C-6AFF-C134-67A1-BC28FDD7ACF1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {3BA8A38C-6AFF-C134-67A1-BC28FDD7ACF1}.Debug|x86.Build.0 = Debug|Any CPU
+ {3BA8A38C-6AFF-C134-67A1-BC28FDD7ACF1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3BA8A38C-6AFF-C134-67A1-BC28FDD7ACF1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3BA8A38C-6AFF-C134-67A1-BC28FDD7ACF1}.Release|x64.ActiveCfg = Release|Any CPU
+ {3BA8A38C-6AFF-C134-67A1-BC28FDD7ACF1}.Release|x64.Build.0 = Release|Any CPU
+ {3BA8A38C-6AFF-C134-67A1-BC28FDD7ACF1}.Release|x86.ActiveCfg = Release|Any CPU
+ {3BA8A38C-6AFF-C134-67A1-BC28FDD7ACF1}.Release|x86.Build.0 = Release|Any CPU
+ {6D9BB2AD-FAB4-4C5C-97FC-A20AFD3AA13A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6D9BB2AD-FAB4-4C5C-97FC-A20AFD3AA13A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6D9BB2AD-FAB4-4C5C-97FC-A20AFD3AA13A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6D9BB2AD-FAB4-4C5C-97FC-A20AFD3AA13A}.Debug|x64.Build.0 = Debug|Any CPU
+ {6D9BB2AD-FAB4-4C5C-97FC-A20AFD3AA13A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6D9BB2AD-FAB4-4C5C-97FC-A20AFD3AA13A}.Debug|x86.Build.0 = Debug|Any CPU
+ {6D9BB2AD-FAB4-4C5C-97FC-A20AFD3AA13A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6D9BB2AD-FAB4-4C5C-97FC-A20AFD3AA13A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6D9BB2AD-FAB4-4C5C-97FC-A20AFD3AA13A}.Release|x64.ActiveCfg = Release|Any CPU
+ {6D9BB2AD-FAB4-4C5C-97FC-A20AFD3AA13A}.Release|x64.Build.0 = Release|Any CPU
+ {6D9BB2AD-FAB4-4C5C-97FC-A20AFD3AA13A}.Release|x86.ActiveCfg = Release|Any CPU
+ {6D9BB2AD-FAB4-4C5C-97FC-A20AFD3AA13A}.Release|x86.Build.0 = Release|Any CPU
+ {9EA9ED81-535D-44CA-8322-D4FB841E17CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9EA9ED81-535D-44CA-8322-D4FB841E17CB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9EA9ED81-535D-44CA-8322-D4FB841E17CB}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9EA9ED81-535D-44CA-8322-D4FB841E17CB}.Debug|x64.Build.0 = Debug|Any CPU
+ {9EA9ED81-535D-44CA-8322-D4FB841E17CB}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9EA9ED81-535D-44CA-8322-D4FB841E17CB}.Debug|x86.Build.0 = Debug|Any CPU
+ {9EA9ED81-535D-44CA-8322-D4FB841E17CB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9EA9ED81-535D-44CA-8322-D4FB841E17CB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9EA9ED81-535D-44CA-8322-D4FB841E17CB}.Release|x64.ActiveCfg = Release|Any CPU
+ {9EA9ED81-535D-44CA-8322-D4FB841E17CB}.Release|x64.Build.0 = Release|Any CPU
+ {9EA9ED81-535D-44CA-8322-D4FB841E17CB}.Release|x86.ActiveCfg = Release|Any CPU
+ {9EA9ED81-535D-44CA-8322-D4FB841E17CB}.Release|x86.Build.0 = Release|Any CPU
+ {1C8C5E5F-8C5C-4F3F-B526-C9F241E598F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1C8C5E5F-8C5C-4F3F-B526-C9F241E598F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1C8C5E5F-8C5C-4F3F-B526-C9F241E598F1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1C8C5E5F-8C5C-4F3F-B526-C9F241E598F1}.Debug|x64.Build.0 = Debug|Any CPU
+ {1C8C5E5F-8C5C-4F3F-B526-C9F241E598F1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1C8C5E5F-8C5C-4F3F-B526-C9F241E598F1}.Debug|x86.Build.0 = Debug|Any CPU
+ {1C8C5E5F-8C5C-4F3F-B526-C9F241E598F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1C8C5E5F-8C5C-4F3F-B526-C9F241E598F1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1C8C5E5F-8C5C-4F3F-B526-C9F241E598F1}.Release|x64.ActiveCfg = Release|Any CPU
+ {1C8C5E5F-8C5C-4F3F-B526-C9F241E598F1}.Release|x64.Build.0 = Release|Any CPU
+ {1C8C5E5F-8C5C-4F3F-B526-C9F241E598F1}.Release|x86.ActiveCfg = Release|Any CPU
+ {1C8C5E5F-8C5C-4F3F-B526-C9F241E598F1}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/CreditApp.Api/Controllers/CreditController.cs b/CreditApp.Api/Controllers/CreditController.cs
new file mode 100644
index 00000000..af087e73
--- /dev/null
+++ b/CreditApp.Api/Controllers/CreditController.cs
@@ -0,0 +1,61 @@
+using CreditApp.Api.Services;
+using CreditApp.Domain.Data;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CreditApp.Api.Controllers;
+
+///
+/// Контроллер для работы с кредитными заявками
+///
+[ApiController]
+[Route("api/[controller]")]
+public class CreditController(
+ ICreditService creditService,
+ ILogger logger)
+ : ControllerBase
+{
+ ///
+ /// Получить кредитную заявку по идентификатору
+ ///
+ [HttpGet]
+ [ProducesResponseType(typeof(CreditApplication), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ [ProducesResponseType(StatusCodes.Status500InternalServerError)]
+ public async Task> Get(
+ int id,
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ if (id <= 0)
+ {
+ logger.LogWarning("Invalid credit application ID: {CreditId}", id);
+ return BadRequest("Id must be positive number");
+ }
+
+ logger.LogInformation("Requesting credit application {CreditId}", id);
+
+ var result = await creditService.GetAsync(id, cancellationToken);
+
+ if (result == null)
+ {
+ logger.LogWarning("Credit application {CreditId} not found", id);
+ return NotFound($"Credit application with ID {id} not found");
+ }
+
+ logger.LogInformation("Successfully retrieved credit application {CreditId}", id);
+ return Ok(result);
+ }
+ catch (OperationCanceledException)
+ {
+ logger.LogWarning("Request for credit application {CreditId} was cancelled", id);
+ return StatusCode(499, "Request cancelled by client");
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error retrieving credit application {CreditId}: {ErrorMessage}", id, ex.Message);
+ return StatusCode(StatusCodes.Status500InternalServerError, "Internal server error");
+ }
+ }
+}
\ No newline at end of file
diff --git a/CreditApp.Api/CreditApp.Api.csproj b/CreditApp.Api/CreditApp.Api.csproj
new file mode 100644
index 00000000..a3c19111
--- /dev/null
+++ b/CreditApp.Api/CreditApp.Api.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CreditApp.Api/Program.cs b/CreditApp.Api/Program.cs
new file mode 100644
index 00000000..291ee912
--- /dev/null
+++ b/CreditApp.Api/Program.cs
@@ -0,0 +1,32 @@
+using Amazon.SQS;
+using CreditApp.Api.Services;
+using CreditApp.ServiceDefaults;
+
+var builder = WebApplication.CreateBuilder(args);
+builder.AddServiceDefaults();
+
+builder.AddRedisDistributedCache("redis");
+
+builder.Services.AddControllers();
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+
+var localstackUrl = "http://sqs.us-east-1.localhost.localstack.cloud:4566";
+var sqsConfig = new AmazonSQSConfig { ServiceURL = localstackUrl };
+builder.Services.AddSingleton(new AmazonSQSClient("test", "test", sqsConfig));
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+
+var app = builder.Build();
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.MapDefaultEndpoints();
+app.UseHttpsRedirection();
+app.UseAuthorization();
+app.MapControllers();
+app.Run();
\ No newline at end of file
diff --git a/CreditApp.Api/Properties/launchSettings.json b/CreditApp.Api/Properties/launchSettings.json
new file mode 100644
index 00000000..79cb38c9
--- /dev/null
+++ b/CreditApp.Api/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:50546",
+ "sslPort": 44330
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:7401",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7401",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/CreditApp.Api/Services/CreditGenerator.cs b/CreditApp.Api/Services/CreditGenerator.cs
new file mode 100644
index 00000000..02328934
--- /dev/null
+++ b/CreditApp.Api/Services/CreditGenerator.cs
@@ -0,0 +1,62 @@
+using Bogus;
+using CreditApp.Domain.Data;
+
+namespace CreditApp.Api.Services;
+
+///
+/// Генератор тестовых данных для кредитных заявок.
+///
+public static class CreditGenerator
+{
+ private const double CbRate = 16.0;
+
+ private static readonly string[] _statuses =
+ {
+ "Новая",
+ "В обработке",
+ "Одобрена",
+ "Отклонена"
+ };
+
+ private static readonly string[] _types =
+ {
+ "Потребительский",
+ "Ипотека",
+ "Автокредит"
+ };
+
+ private static readonly Faker _faker =
+ new Faker()
+ .RuleFor(x => x.Id, f => f.IndexFaker)
+ .RuleFor(x => x.CreditType, f => f.PickRandom(_types))
+ .RuleFor(x => x.RequestedAmount,
+ f => Math.Round(f.Random.Decimal(10_000, 5_000_000), 2))
+ .RuleFor(x => x.TermMonths,
+ f => f.Random.Int(6, 360))
+ .RuleFor(x => x.InterestRate,
+ f => Math.Round(f.Random.Double(CbRate, CbRate + 5), 2))
+ .RuleFor(x => x.ApplicationDate,
+ f => DateOnly.FromDateTime(f.Date.Past(2)))
+ .RuleFor(x => x.HasInsurance,
+ f => f.Random.Bool())
+ .RuleFor(x => x.Status,
+ f => f.PickRandom(_statuses))
+ .RuleFor(x => x.DecisionDate, (f, x) =>
+ x.Status is "Одобрена" or "Отклонена"
+ ? DateOnly.FromDateTime(
+ f.Date.Between(
+ x.ApplicationDate.ToDateTime(TimeOnly.MinValue),
+ DateTime.Now))
+ : null)
+ .RuleFor(x => x.ApprovedAmount, (f, x) =>
+ x.Status == "Одобрена"
+ ? Math.Round(f.Random.Decimal(10_000, x.RequestedAmount), 2)
+ : null);
+
+ public static CreditApplication Generate(int id)
+ {
+ var result = _faker.Generate();
+ result.Id = id;
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/CreditApp.Api/Services/CreditService.cs b/CreditApp.Api/Services/CreditService.cs
new file mode 100644
index 00000000..c4c2b97e
--- /dev/null
+++ b/CreditApp.Api/Services/CreditService.cs
@@ -0,0 +1,77 @@
+using CreditApp.Domain.Data;
+using CreditApp.Messaging.Contracts;
+using Microsoft.Extensions.Caching.Distributed;
+using System.Text.Json;
+
+namespace CreditApp.Api.Services;
+
+///
+/// Сервис для работы с кредитными заявками.
+///
+public class CreditService(
+ IDistributedCache cache,
+ ILogger logger,
+ SqsProducer sqsProducer)
+ : ICreditService
+{
+ private const string CachePrefix = "credit:";
+
+ public async Task GetAsync(
+ int id,
+ CancellationToken cancellationToken = default)
+ {
+ var key = $"{CachePrefix}{id}";
+
+ var cached = await cache.GetStringAsync(key, cancellationToken);
+
+ if (cached is not null)
+ {
+ logger.LogInformation(
+ "Cache HIT for credit application {CreditId}",
+ id);
+
+ return JsonSerializer.Deserialize(cached)!;
+ }
+
+ logger.LogInformation(
+ "Cache MISS for credit application {CreditId}",
+ id);
+
+ var result = CreditGenerator.Generate(id);
+ var creditEvent = new CreditGeneratedEvent
+ {
+ Id = result.Id,
+ CreditType = result.CreditType,
+ RequestedAmount = result.RequestedAmount,
+ TermMonths = result.TermMonths,
+ InterestRate = result.InterestRate,
+ ApplicationDate = result.ApplicationDate,
+ HasInsurance = result.HasInsurance,
+ Status = result.Status,
+ DecisionDate = result.DecisionDate,
+ ApprovedAmount = result.ApprovedAmount,
+ GeneratedAt = DateTime.UtcNow
+ };
+
+ await sqsProducer.PublishAsync(creditEvent);
+
+ var serialized = JsonSerializer.Serialize(result);
+
+ var options = new DistributedCacheEntryOptions
+ {
+ AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10)
+ };
+
+ await cache.SetStringAsync(key, serialized, options, cancellationToken);
+
+
+ logger.LogInformation(
+ "Generated credit application {CreditId}. Type: {Type}, Amount: {Amount}, Status: {Status}",
+ result.Id,
+ result.CreditType,
+ result.RequestedAmount,
+ result.Status);
+
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/CreditApp.Api/Services/ICreditService.cs b/CreditApp.Api/Services/ICreditService.cs
new file mode 100644
index 00000000..aca16dcc
--- /dev/null
+++ b/CreditApp.Api/Services/ICreditService.cs
@@ -0,0 +1,16 @@
+using CreditApp.Domain.Data;
+
+namespace CreditApp.Api.Services;
+
+///
+/// Интерфейс сервиса для работы с кредитными заявками
+///
+public interface ICreditService
+{
+ ///
+ /// Получить кредитную заявку по идентификатору
+ ///
+ public Task GetAsync(
+ int id,
+ CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/CreditApp.Api/Services/SqsProducer.cs b/CreditApp.Api/Services/SqsProducer.cs
new file mode 100644
index 00000000..4049c626
--- /dev/null
+++ b/CreditApp.Api/Services/SqsProducer.cs
@@ -0,0 +1,33 @@
+using Amazon.SQS;
+using Amazon.SQS.Model;
+using CreditApp.Messaging.Contracts;
+using System.Text.Json;
+
+namespace CreditApp.Api.Services;
+
+///
+/// Продюсер для отправки сообщений в очередь SQS (LocalStack)
+///
+public class SqsProducer
+{
+ private readonly IAmazonSQS _sqs;
+ private readonly ILogger _logger;
+ private readonly string _queueUrl = "http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/credit-queue";
+
+ public SqsProducer(IAmazonSQS sqs, ILogger logger)
+ {
+ _sqs = sqs;
+ _logger = logger;
+ }
+
+ ///
+ /// Публикует событие о сгенерированной кредитной заявке в очередь SQS
+ ///
+ public async Task PublishAsync(CreditGeneratedEvent creditEvent)
+ {
+ var message = JsonSerializer.Serialize(creditEvent);
+ var request = new SendMessageRequest { QueueUrl = _queueUrl, MessageBody = message };
+ await _sqs.SendMessageAsync(request);
+ _logger.LogInformation("Published credit {Id} to SQS", creditEvent.Id);
+ }
+}
\ No newline at end of file
diff --git a/CreditApp.Api/appsettings.Development.json b/CreditApp.Api/appsettings.Development.json
new file mode 100644
index 00000000..0c208ae9
--- /dev/null
+++ b/CreditApp.Api/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/CreditApp.Api/appsettings.json b/CreditApp.Api/appsettings.json
new file mode 100644
index 00000000..3ea34a29
--- /dev/null
+++ b/CreditApp.Api/appsettings.json
@@ -0,0 +1,10 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "SQS_URL": "http://localhost:4566"
+}
\ No newline at end of file
diff --git a/CreditApp.AppHost/AppHost.cs b/CreditApp.AppHost/AppHost.cs
new file mode 100644
index 00000000..fd1ec8e7
--- /dev/null
+++ b/CreditApp.AppHost/AppHost.cs
@@ -0,0 +1,52 @@
+var builder = DistributedApplication.CreateBuilder(args);
+
+var localstackToken = builder.Configuration["LocalStack:AuthToken"];
+
+var redis = builder.AddRedis("redis")
+ .WithRedisCommander(containerName: "redis-commander");
+
+var localstack = builder.AddContainer("localstack", "localstack/localstack:latest")
+ .WithEnvironment("LOCALSTACK_AUTH_TOKEN", localstackToken)
+ .WithEnvironment("SERVICES", "s3,sqs")
+ .WithEnvironment("AWS_DEFAULT_REGION", "us-east-1")
+ .WithEnvironment("AWS_ACCESS_KEY_ID", "test")
+ .WithEnvironment("AWS_SECRET_ACCESS_KEY", "test")
+ .WithEndpoint(port: 4566, targetPort: 4566, name: "api")
+ .WithLifetime(ContainerLifetime.Persistent);
+
+var gateway = builder.AddProject("gateway")
+ .WithEndpoint("https", e =>
+ {
+ e.Port = 9002;
+ e.IsProxied = false;
+ e.UriScheme = "https";
+ })
+ .WithExternalHttpEndpoints();
+
+for (var i = 0; i < 5; i++)
+{
+ var port = 7401 + i;
+ var api = builder.AddProject($"api{i + 1}")
+ .WithEndpoint("https", e =>
+ {
+ e.Port = port;
+ e.IsProxied = false;
+ e.UriScheme = "https";
+ })
+ .WithReference(redis)
+ .WithEnvironment("LOCALSTACK_URL", "http://localhost:4566")
+ .WaitFor(redis)
+ .WaitFor(localstack);
+
+ gateway.WaitFor(api);
+}
+
+builder.AddProject("fileservice")
+ .WithEnvironment("LOCALSTACK_URL", "http://localhost:4566")
+ .WaitFor(localstack);
+
+builder.AddProject("client")
+ .WithReference(gateway)
+ .WithExternalHttpEndpoints();
+
+builder.Build().Run();
\ No newline at end of file
diff --git a/CreditApp.AppHost/CreditApp.AppHost.csproj b/CreditApp.AppHost/CreditApp.AppHost.csproj
new file mode 100644
index 00000000..c2cde5b6
--- /dev/null
+++ b/CreditApp.AppHost/CreditApp.AppHost.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ b2dc854c-a0d1-4a18-8fc3-d0168e660b49
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CreditApp.AppHost/Properties/launchSettings.json b/CreditApp.AppHost/Properties/launchSettings.json
new file mode 100644
index 00000000..4af77347
--- /dev/null
+++ b/CreditApp.AppHost/Properties/launchSettings.json
@@ -0,0 +1,31 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "https://localhost:17244;http://localhost:15135",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21273",
+ "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "https://localhost:23257",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22263"
+ }
+ },
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15135",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19038",
+ "ASPIRE_DASHBOARD_MCP_ENDPOINT_URL": "http://localhost:18140",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20023"
+ }
+ }
+ }
+}
diff --git a/CreditApp.AppHost/appsettings.json b/CreditApp.AppHost/appsettings.json
new file mode 100644
index 00000000..80a0d478
--- /dev/null
+++ b/CreditApp.AppHost/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ },
+ "LocalStack": {
+ "AuthToken": ""
+ }
+}
\ No newline at end of file
diff --git a/CreditApp.Domain/CreditApp.Domain.csproj b/CreditApp.Domain/CreditApp.Domain.csproj
new file mode 100644
index 00000000..e1b91f91
--- /dev/null
+++ b/CreditApp.Domain/CreditApp.Domain.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
\ No newline at end of file
diff --git a/CreditApp.Domain/Data/CreditApplication.cs b/CreditApp.Domain/Data/CreditApplication.cs
new file mode 100644
index 00000000..4366e487
--- /dev/null
+++ b/CreditApp.Domain/Data/CreditApplication.cs
@@ -0,0 +1,48 @@
+namespace CreditApp.Domain.Data;
+
+///
+/// Класс, представляющий кредитную заявку
+///
+public class CreditApplication
+{
+ ///
+ /// Идентификатор в системе
+ ///
+ public int Id { get; set; }
+ ///
+ /// Тип кредита
+ ///
+ public string CreditType { get; set; } = default!;
+ ///
+ /// Запрашиваемая сумма
+ ///
+ public decimal RequestedAmount { get; set; }
+ ///
+ /// Срок в месяцах
+ ///
+ public int TermMonths { get; set; }
+ ///
+ /// Процентная ставка
+ ///
+ public double InterestRate { get; set; }
+ ///
+ /// Дата подачи
+ ///
+ public DateOnly ApplicationDate { get; set; }
+ ///
+ /// Необходимость страховки
+ ///
+ public bool HasInsurance { get; set; }
+ ///
+ /// Статус заявки
+ ///
+ public string Status { get; set; } = default!;
+ ///
+ /// Дата решения
+ ///
+ public DateOnly? DecisionDate { get; set; }
+ ///
+ /// Одобренная сумма
+ ///
+ public decimal? ApprovedAmount { get; set; }
+}
diff --git a/CreditApp.FileService/CreditApp.FileService.csproj b/CreditApp.FileService/CreditApp.FileService.csproj
new file mode 100644
index 00000000..da809b3a
--- /dev/null
+++ b/CreditApp.FileService/CreditApp.FileService.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CreditApp.FileService/CreditApp.FileService.http b/CreditApp.FileService/CreditApp.FileService.http
new file mode 100644
index 00000000..92650254
--- /dev/null
+++ b/CreditApp.FileService/CreditApp.FileService.http
@@ -0,0 +1,6 @@
+@CreditApp.FileService_HostAddress = http://localhost:5065
+
+GET {{CreditApp.FileService_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/CreditApp.FileService/IFileStorage.cs b/CreditApp.FileService/IFileStorage.cs
new file mode 100644
index 00000000..f4a5c618
--- /dev/null
+++ b/CreditApp.FileService/IFileStorage.cs
@@ -0,0 +1,11 @@
+namespace CreditApp.FileService;
+
+///
+/// Интерфейс для работы с файловым хранилищем
+///
+public interface IFileStorage
+{
+ public Task SaveAsync(string bucketName, string fileName, byte[] data, CancellationToken cancellationToken = default);
+
+ public Task GetAsync(string bucketName, string fileName, CancellationToken cancellationToken = default);
+}
\ No newline at end of file
diff --git a/CreditApp.FileService/Program.cs b/CreditApp.FileService/Program.cs
new file mode 100644
index 00000000..8d4313dd
--- /dev/null
+++ b/CreditApp.FileService/Program.cs
@@ -0,0 +1,40 @@
+using Amazon.S3;
+using Amazon.SQS;
+using CreditApp.FileService;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.Services.AddControllers();
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+
+var localstackUrl = builder.Configuration["LOCALSTACK_URL"] ?? "http://localhost:4566";
+
+var s3Config = new AmazonS3Config
+{
+ ServiceURL = localstackUrl,
+ ForcePathStyle = true,
+ UseHttp = true
+};
+builder.Services.AddSingleton(new AmazonS3Client("test", "test", s3Config));
+
+var sqsConfig = new AmazonSQSConfig
+{
+ ServiceURL = localstackUrl,
+ UseHttp = true
+};
+builder.Services.AddSingleton(new AmazonSQSClient("test", "test", sqsConfig));
+
+builder.Services.AddScoped();
+builder.Services.AddHostedService();
+
+var app = builder.Build();
+
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.MapControllers();
+app.Run();
\ No newline at end of file
diff --git a/CreditApp.FileService/Properties/launchSettings.json b/CreditApp.FileService/Properties/launchSettings.json
new file mode 100644
index 00000000..64707d02
--- /dev/null
+++ b/CreditApp.FileService/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:57163",
+ "sslPort": 44325
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5065",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7288;http://localhost:5065",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/CreditApp.FileService/Services/S3Storage.cs b/CreditApp.FileService/Services/S3Storage.cs
new file mode 100644
index 00000000..169cf540
--- /dev/null
+++ b/CreditApp.FileService/Services/S3Storage.cs
@@ -0,0 +1,73 @@
+using Amazon.S3;
+using Amazon.S3.Model;
+
+using CreditApp.FileService;
+
+///
+/// Хранилище файлов в S3 (LocalStack)
+///
+public class S3Storage : IFileStorage
+{
+ private readonly IAmazonS3 _s3Client;
+ private readonly ILogger _logger;
+
+ public S3Storage(IAmazonS3 s3Client, ILogger logger)
+ {
+ _s3Client = s3Client;
+ _logger = logger;
+ }
+
+ ///
+ /// Сохраняет файл в S3 bucket
+ ///
+ public async Task SaveAsync(string bucketName, string fileName, byte[] data, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ var buckets = await _s3Client.ListBucketsAsync(cancellationToken);
+ if (!buckets.Buckets.Any(b => b.BucketName == bucketName))
+ {
+ await _s3Client.PutBucketAsync(new PutBucketRequest { BucketName = bucketName }, cancellationToken);
+ _logger.LogInformation("Bucket {BucketName} created", bucketName);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "Error checking/creating bucket");
+ }
+
+ using var stream = new MemoryStream(data);
+
+ var request = new PutObjectRequest
+ {
+ BucketName = bucketName,
+ Key = fileName,
+ InputStream = stream,
+ ContentType = "application/json",
+ UseChunkEncoding = false,
+ AutoCloseStream = true
+ };
+
+ await _s3Client.PutObjectAsync(request, cancellationToken);
+ _logger.LogInformation("Saved {FileName} to S3 bucket {BucketName}", fileName, bucketName);
+ }
+
+ ///
+ /// Получает файл из S3 bucket
+ ///
+ public async Task GetAsync(string bucketName, string fileName, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ var request = new GetObjectRequest { BucketName = bucketName, Key = fileName };
+ using var response = await _s3Client.GetObjectAsync(request, cancellationToken);
+ using var ms = new MemoryStream();
+ await response.ResponseStream.CopyToAsync(ms, cancellationToken);
+ return ms.ToArray();
+ }
+ catch (AmazonS3Exception ex) when (ex.StatusCode == System.Net.HttpStatusCode.NotFound)
+ {
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/CreditApp.FileService/Services/SqsConsumer.cs b/CreditApp.FileService/Services/SqsConsumer.cs
new file mode 100644
index 00000000..1eb8e917
--- /dev/null
+++ b/CreditApp.FileService/Services/SqsConsumer.cs
@@ -0,0 +1,81 @@
+using Amazon.SQS;
+using Amazon.SQS.Model;
+using CreditApp.FileService;
+using CreditApp.Messaging.Contracts;
+using System.Text;
+using System.Text.Json;
+
+///
+/// Consumer для получения сообщений из очереди SQS и сохранения в S3
+///
+public class SqsConsumer : BackgroundService
+{
+ private readonly IAmazonSQS _sqs;
+ private readonly IServiceProvider _serviceProvider;
+ private readonly ILogger _logger;
+ private readonly string _queueUrl = "http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/credit-queue";
+
+ public SqsConsumer(IAmazonSQS sqs, IServiceProvider serviceProvider, ILogger logger)
+ {
+ _sqs = sqs;
+ _serviceProvider = serviceProvider;
+ _logger = logger;
+ }
+
+ ///
+ /// Фоновый процесс получения сообщений из очереди
+ ///
+ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
+ {
+ while (!stoppingToken.IsCancellationRequested)
+ {
+ try
+ {
+ var receiveRequest = new ReceiveMessageRequest
+ {
+ QueueUrl = _queueUrl,
+ MaxNumberOfMessages = 10,
+ WaitTimeSeconds = 20
+ };
+
+ var response = await _sqs.ReceiveMessageAsync(receiveRequest, stoppingToken);
+
+ if (response.Messages != null)
+ {
+ foreach (var message in response.Messages)
+ {
+ try
+ {
+ var creditEvent = JsonSerializer.Deserialize(message.Body);
+ if (creditEvent != null)
+ {
+ using var scope = _serviceProvider.CreateScope();
+ var storage = scope.ServiceProvider.GetRequiredService();
+
+ var json = JsonSerializer.Serialize(creditEvent);
+ var data = Encoding.UTF8.GetBytes(json);
+ var fileName = $"credit_{creditEvent.Id}_{DateTime.Now:yyyyMMddHHmmss}.json";
+
+ await storage.SaveAsync("credit-applications", fileName, data, stoppingToken);
+ _logger.LogInformation("Saved credit {Id} to S3", creditEvent.Id);
+ }
+
+ await _sqs.DeleteMessageAsync(_queueUrl, message.ReceiptHandle, stoppingToken);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error processing message");
+ }
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Error receiving messages");
+ await Task.Delay(5000, stoppingToken);
+ }
+
+ await Task.Delay(1000, stoppingToken);
+ }
+ }
+}
\ No newline at end of file
diff --git a/CreditApp.FileService/appsettings.Development.json b/CreditApp.FileService/appsettings.Development.json
new file mode 100644
index 00000000..0c208ae9
--- /dev/null
+++ b/CreditApp.FileService/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/CreditApp.FileService/appsettings.json b/CreditApp.FileService/appsettings.json
new file mode 100644
index 00000000..03cfbc35
--- /dev/null
+++ b/CreditApp.FileService/appsettings.json
@@ -0,0 +1,13 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "SQS_URL": "http://localhost:9324",
+ "SQS_QUEUE_URL": "http://localhost:9324/queue/credit-queue",
+ "MINIO_URL": "localhost:9001",
+ "MINIO_ACCESS_KEY": "minioadmin",
+ "MINIO_SECRET_KEY": "minioadmin"
+}
\ No newline at end of file
diff --git a/CreditApp.Gateway/CreditApp.Gateway.csproj b/CreditApp.Gateway/CreditApp.Gateway.csproj
new file mode 100644
index 00000000..a0efda0a
--- /dev/null
+++ b/CreditApp.Gateway/CreditApp.Gateway.csproj
@@ -0,0 +1,17 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CreditApp.Gateway/LoadBalancers/WeightedRoundRobinCreator.cs b/CreditApp.Gateway/LoadBalancers/WeightedRoundRobinCreator.cs
new file mode 100644
index 00000000..67f3886f
--- /dev/null
+++ b/CreditApp.Gateway/LoadBalancers/WeightedRoundRobinCreator.cs
@@ -0,0 +1,29 @@
+using Ocelot.Configuration;
+using Ocelot.LoadBalancer.Interfaces;
+using Ocelot.Responses;
+using Ocelot.ServiceDiscovery.Providers;
+using Ocelot.Values;
+
+namespace CreditApp.Gateway.LoadBalancers;
+
+///
+/// Создатель балансировщика нагрузки WeightedRoundRobin для Ocelot
+///
+public class WeightedRoundRobinCreator : ILoadBalancerCreator
+{
+ public string Type => nameof(WeightedRoundRobinCreator).Replace("Creator", "");
+ public Response Create(
+ DownstreamRoute route,
+ IServiceDiscoveryProvider serviceProvider)
+ {
+ var services = serviceProvider.GetAsync().Result;
+
+ var hostAndPorts = services
+ .Select(s => s.HostAndPort)
+ .ToList();
+
+ var balancer = new WeightedRoundRobinLoadBalancer(hostAndPorts);
+
+ return new OkResponse(balancer);
+ }
+}
\ No newline at end of file
diff --git a/CreditApp.Gateway/LoadBalancers/WeightedRoundRobinLoadBalancer.cs b/CreditApp.Gateway/LoadBalancers/WeightedRoundRobinLoadBalancer.cs
new file mode 100644
index 00000000..09bdf7ad
--- /dev/null
+++ b/CreditApp.Gateway/LoadBalancers/WeightedRoundRobinLoadBalancer.cs
@@ -0,0 +1,46 @@
+using Ocelot.LoadBalancer;
+using Ocelot.LoadBalancer.Interfaces;
+using Ocelot.Responses;
+using Ocelot.Values;
+
+namespace CreditApp.Gateway.LoadBalancers;
+
+///
+/// Балансировщик нагрузки, реализующий алгоритм Weighted Round Robin
+///
+public class WeightedRoundRobinLoadBalancer : ILoadBalancer
+{
+ private readonly List _sequence;
+ private int _index = -1;
+ private readonly object _lock = new();
+ private readonly string _type;
+ public string Type => _type;
+ public WeightedRoundRobinLoadBalancer(List services)
+ {
+ _type = nameof(WeightedRoundRobinLoadBalancer).Replace("LoadBalancer", "");
+
+ var weights = new[] { 3, 2, 1, 1, 1 };
+ _sequence = [];
+
+ for (var i = 0; i < services.Count; i++)
+ {
+ var weight = weights[i];
+ for (var j = 0; j < weight; j++)
+ {
+ _sequence.Add(services[i]);
+ }
+ }
+ }
+ public Task> LeaseAsync(HttpContext context)
+ {
+ lock (_lock)
+ {
+ _index = (_index + 1) % _sequence.Count;
+ return Task.FromResult>(
+ new OkResponse(_sequence[_index]));
+ }
+ }
+ public void Release(ServiceHostAndPort hostAndPort)
+ {
+ }
+}
\ No newline at end of file
diff --git a/CreditApp.Gateway/Program.cs b/CreditApp.Gateway/Program.cs
new file mode 100644
index 00000000..6f94b8a1
--- /dev/null
+++ b/CreditApp.Gateway/Program.cs
@@ -0,0 +1,30 @@
+using CreditApp.Gateway.LoadBalancers;
+using CreditApp.ServiceDefaults;
+using Ocelot.DependencyInjection;
+using Ocelot.LoadBalancer.Interfaces;
+using Ocelot.Middleware;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true);
+
+builder.Services.AddCors(options =>
+{
+ options.AddPolicy("wasm", policy =>
+ {
+ policy.AllowAnyOrigin()
+ .WithMethods("GET")
+ .WithHeaders("Content-Type");
+ });
+});
+
+builder.Services.AddOcelot();
+builder.Services.AddSingleton();
+
+var app = builder.Build();
+
+app.UseCors("wasm");
+app.UseHttpsRedirection();
+await app.UseOcelot();
+app.Run();
\ No newline at end of file
diff --git a/CreditApp.Gateway/Properties/launchSettings.json b/CreditApp.Gateway/Properties/launchSettings.json
new file mode 100644
index 00000000..e4479f98
--- /dev/null
+++ b/CreditApp.Gateway/Properties/launchSettings.json
@@ -0,0 +1,38 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:42841",
+ "sslPort": 44351
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "http://localhost:9001",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": false,
+ "applicationUrl": "https://localhost:9001",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/CreditApp.Gateway/appsettings.Development.json b/CreditApp.Gateway/appsettings.Development.json
new file mode 100644
index 00000000..0c208ae9
--- /dev/null
+++ b/CreditApp.Gateway/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/CreditApp.Gateway/appsettings.json b/CreditApp.Gateway/appsettings.json
new file mode 100644
index 00000000..10f68b8c
--- /dev/null
+++ b/CreditApp.Gateway/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/CreditApp.Gateway/ocelot.json b/CreditApp.Gateway/ocelot.json
new file mode 100644
index 00000000..3792c947
--- /dev/null
+++ b/CreditApp.Gateway/ocelot.json
@@ -0,0 +1,38 @@
+{
+ "Routes": [
+ {
+ "DownstreamPathTemplate": "/api/Credit",
+ "DownstreamScheme": "https",
+ "DownstreamHostAndPorts": [
+ {
+ "Host": "localhost",
+ "Port": 7401
+ },
+ {
+ "Host": "localhost",
+ "Port": 7402
+ },
+ {
+ "Host": "localhost",
+ "Port": 7403
+ },
+ {
+ "Host": "localhost",
+ "Port": 7404
+ },
+ {
+ "Host": "localhost",
+ "Port": 7405
+ }
+ ],
+ "UpstreamHttpMethod": [ "GET" ],
+ "UpstreamPathTemplate": "/api/Credit",
+ "LoadBalancerOptions": {
+ "Type": "WeightedRoundRobin"
+ }
+ }
+ ],
+ "GlobalConfiguration": {
+ "BaseUrl": "https://localhost:9001"
+ }
+}
\ No newline at end of file
diff --git a/CreditApp.Messaging/Contracts/CreditGeneratedEvent.cs b/CreditApp.Messaging/Contracts/CreditGeneratedEvent.cs
new file mode 100644
index 00000000..8c2e8ce7
--- /dev/null
+++ b/CreditApp.Messaging/Contracts/CreditGeneratedEvent.cs
@@ -0,0 +1,30 @@
+namespace CreditApp.Messaging.Contracts;
+
+///
+/// Событие, возникающее при генерации новой кредитной заявки.
+/// Используется для обмена данными между сервисами через брокер сообщений SQS.
+///
+public class CreditGeneratedEvent
+{
+ public int Id { get; set; }
+
+ public string CreditType { get; set; } = string.Empty;
+
+ public decimal RequestedAmount { get; set; }
+
+ public int TermMonths { get; set; }
+
+ public double InterestRate { get; set; }
+
+ public DateOnly ApplicationDate { get; set; }
+
+ public bool HasInsurance { get; set; }
+
+ public string Status { get; set; } = string.Empty;
+
+ public DateOnly? DecisionDate { get; set; }
+
+ public decimal? ApprovedAmount { get; set; }
+
+ public DateTime GeneratedAt { get; set; }
+}
\ No newline at end of file
diff --git a/CreditApp.Messaging/CreditApp.Messaging.csproj b/CreditApp.Messaging/CreditApp.Messaging.csproj
new file mode 100644
index 00000000..fa71b7ae
--- /dev/null
+++ b/CreditApp.Messaging/CreditApp.Messaging.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
diff --git a/CreditApp.ServiceDefaults/CreditApp.ServiceDefaults.csproj b/CreditApp.ServiceDefaults/CreditApp.ServiceDefaults.csproj
new file mode 100644
index 00000000..8ad67261
--- /dev/null
+++ b/CreditApp.ServiceDefaults/CreditApp.ServiceDefaults.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ enable
+ enable
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CreditApp.ServiceDefaults/Extensions.cs b/CreditApp.ServiceDefaults/Extensions.cs
new file mode 100644
index 00000000..c5724030
--- /dev/null
+++ b/CreditApp.ServiceDefaults/Extensions.cs
@@ -0,0 +1,128 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Diagnostics.HealthChecks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Diagnostics.HealthChecks;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.ServiceDiscovery;
+using OpenTelemetry;
+using OpenTelemetry.Metrics;
+using OpenTelemetry.Trace;
+
+namespace CreditApp.ServiceDefaults;
+
+// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
+// This project should be referenced by each service project in your solution.
+// To learn more about using this project, see https://aka.ms/dotnet/aspire/service-defaults
+public static class Extensions
+{
+ private const string HealthEndpointPath = "/health";
+ private const string AlivenessEndpointPath = "/alive";
+
+ public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder
+ {
+ builder.ConfigureOpenTelemetry();
+
+ builder.AddDefaultHealthChecks();
+
+ builder.Services.AddServiceDiscovery();
+
+ builder.Services.ConfigureHttpClientDefaults(http =>
+ {
+ // Turn on resilience by default
+ http.AddStandardResilienceHandler();
+
+ // Turn on service discovery by default
+ http.AddServiceDiscovery();
+ });
+
+ // Uncomment the following to restrict the allowed schemes for service discovery.
+ // builder.Services.Configure(options =>
+ // {
+ // options.AllowedSchemes = ["https"];
+ // });
+
+ return builder;
+ }
+
+ public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder
+ {
+ builder.Logging.AddOpenTelemetry(logging =>
+ {
+ logging.IncludeFormattedMessage = true;
+ logging.IncludeScopes = true;
+ });
+
+ builder.Services.AddOpenTelemetry()
+ .WithMetrics(metrics =>
+ {
+ metrics.AddAspNetCoreInstrumentation()
+ .AddHttpClientInstrumentation()
+ .AddRuntimeInstrumentation();
+ })
+ .WithTracing(tracing =>
+ {
+ tracing.AddSource(builder.Environment.ApplicationName)
+ .AddAspNetCoreInstrumentation(tracing =>
+ // Exclude health check requests from tracing
+ tracing.Filter = context =>
+ !context.Request.Path.StartsWithSegments(HealthEndpointPath)
+ && !context.Request.Path.StartsWithSegments(AlivenessEndpointPath)
+ )
+ // Uncomment the following line to enable gRPC instrumentation (requires the OpenTelemetry.Instrumentation.GrpcNetClient package)
+ //.AddGrpcClientInstrumentation()
+ .AddHttpClientInstrumentation();
+ });
+
+ builder.AddOpenTelemetryExporters();
+
+ return builder;
+ }
+
+ private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder
+ {
+ var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]);
+
+ if (useOtlpExporter)
+ {
+ builder.Services.AddOpenTelemetry().UseOtlpExporter();
+ }
+
+ // Uncomment the following lines to enable the Azure Monitor exporter (requires the Azure.Monitor.OpenTelemetry.AspNetCore package)
+ //if (!string.IsNullOrEmpty(builder.Configuration["APPLICATIONINSIGHTS_CONNECTION_STRING"]))
+ //{
+ // builder.Services.AddOpenTelemetry()
+ // .UseAzureMonitor();
+ //}
+
+ return builder;
+ }
+
+ public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder
+ {
+ builder.Services.AddHealthChecks()
+ // Add a default liveness check to ensure app is responsive
+ .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
+
+ return builder;
+ }
+
+ public static WebApplication MapDefaultEndpoints(this WebApplication app)
+ {
+ // Adding health checks endpoints to applications in non-development environments has security implications.
+ // See https://aka.ms/dotnet/aspire/healthchecks for details before enabling these endpoints in non-development environments.
+ if (app.Environment.IsDevelopment())
+ {
+ // All health checks must pass for app to be considered ready to accept traffic after starting
+ app.MapHealthChecks(HealthEndpointPath);
+
+ // Only health checks tagged with the "live" tag must pass for app to be considered alive
+ app.MapHealthChecks(AlivenessEndpointPath, new HealthCheckOptions
+ {
+ Predicate = r => r.Tags.Contains("live")
+ });
+ }
+
+ return app;
+ }
+}
diff --git a/CreditApp.Tests/CreditApp.Tests.csproj b/CreditApp.Tests/CreditApp.Tests.csproj
new file mode 100644
index 00000000..7e1a660e
--- /dev/null
+++ b/CreditApp.Tests/CreditApp.Tests.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net8.0
+ enable
+ enable
+ false
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CreditApp.Tests/IntegrationTests.cs b/CreditApp.Tests/IntegrationTests.cs
new file mode 100644
index 00000000..e1c6d9bd
--- /dev/null
+++ b/CreditApp.Tests/IntegrationTests.cs
@@ -0,0 +1,79 @@
+using System.Net.Http.Json;
+using CreditApp.Domain.Data;
+using Xunit;
+
+namespace CreditApp.Tests;
+
+///
+/// Интеграционные тесты для проверки работы всех сервисов бекенда
+///
+public class IntegrationTests
+{
+ private readonly HttpClient _client;
+
+ public IntegrationTests()
+ {
+ var handler = new HttpClientHandler
+ {
+ ServerCertificateCustomValidationCallback = (sender, cert, chain, sslPolicyErrors) => true
+ };
+
+ _client = new HttpClient(handler)
+ {
+ BaseAddress = new Uri("https://localhost:9002")
+ };
+ }
+
+ ///
+ /// Проверка полного цикла: генерация кредита и сохранение в S3 через SQS
+ ///
+ [Fact]
+ public async Task FullCycle_GenerateCredit_ShouldSaveToMinIO()
+ {
+ var response = await _client.GetAsync("api/Credit?id=999");
+ response.EnsureSuccessStatusCode();
+
+ var credit = await response.Content.ReadFromJsonAsync();
+ Assert.NotNull(credit);
+ Assert.Equal(999, credit.Id);
+ }
+
+ ///
+ /// Проверка кэширования в Redis (второй запрос должен быть быстрее)
+ ///
+ [Fact]
+ public async Task CacheTest_SecondRequest_ShouldBeFaster()
+ {
+ var response1 = await _client.GetAsync("api/Credit?id=100");
+ response1.EnsureSuccessStatusCode();
+
+ var response2 = await _client.GetAsync("api/Credit?id=100");
+ response2.EnsureSuccessStatusCode();
+
+ Assert.True(true);
+ }
+
+ ///
+ /// Проверка балансировки нагрузки между 5 репликами
+ ///
+ [Fact]
+ public async Task LoadBalancingTest_Requests_ShouldBeDistributed()
+ {
+ for (var i = 1; i <= 8; i++)
+ {
+ var response = await _client.GetAsync($"api/Credit?id={i}");
+ response.EnsureSuccessStatusCode();
+ }
+ Assert.True(true);
+ }
+
+ ///
+ /// Проверка обработки некорректного ID (должен вернуть 400 Bad Request)
+ ///
+ [Fact]
+ public async Task GetCredit_WithInvalidId_ShouldReturnBadRequest()
+ {
+ var response = await _client.GetAsync("api/Credit?id=0");
+ Assert.Equal(System.Net.HttpStatusCode.BadRequest, response.StatusCode);
+ }
+}
\ No newline at end of file