diff --git a/Homeworks/Base/src/PromoCodeFactory.Core/Abstractions/Repositories/IRepository.cs b/Homeworks/Base/src/PromoCodeFactory.Core/Abstractions/Repositories/IRepository.cs index 3fe683e5..c2455aa9 100644 --- a/Homeworks/Base/src/PromoCodeFactory.Core/Abstractions/Repositories/IRepository.cs +++ b/Homeworks/Base/src/PromoCodeFactory.Core/Abstractions/Repositories/IRepository.cs @@ -10,5 +10,11 @@ public interface IRepository where T: BaseEntity Task> GetAllAsync(); Task GetByIdAsync(Guid id); + + Task AddAsync(T entity); + + Task UpdateAsync(T entity); + + Task DeleteAsync(T entity); } } \ No newline at end of file diff --git a/Homeworks/Base/src/PromoCodeFactory.DataAccess/Repositories/InMemoryRepository.cs b/Homeworks/Base/src/PromoCodeFactory.DataAccess/Repositories/InMemoryRepository.cs index 0f1e04d7..a71e5cec 100644 --- a/Homeworks/Base/src/PromoCodeFactory.DataAccess/Repositories/InMemoryRepository.cs +++ b/Homeworks/Base/src/PromoCodeFactory.DataAccess/Repositories/InMemoryRepository.cs @@ -4,25 +4,60 @@ using System.Threading.Tasks; using PromoCodeFactory.Core.Abstractions.Repositories; using PromoCodeFactory.Core.Domain; + namespace PromoCodeFactory.DataAccess.Repositories { public class InMemoryRepository: IRepository where T: BaseEntity { - protected IEnumerable Data { get; set; } + protected ICollection Data { get; set; } - public InMemoryRepository(IEnumerable data) + public InMemoryRepository(ICollection data) { Data = data; } public Task> GetAllAsync() { - return Task.FromResult(Data); + return Task.FromResult>(Data); } public Task GetByIdAsync(Guid id) { return Task.FromResult(Data.FirstOrDefault(x => x.Id == id)); } + + /// + public Task AddAsync(T entity) + { + Data.Add(entity); + return Task.FromResult(entity); + } + + /// + public async Task UpdateAsync(T entity) + { + var oldEntity = await GetByIdAsync(entity.Id); + if (oldEntity == null) + throw new Exception($"Entity with id [{entity.Id}] not found"); + + // stub for in-memory + await DeleteAsync(oldEntity); + await AddAsync(entity); + + // save changes + + return entity; + } + + /// + public Task DeleteAsync(T entity) + { + var storedEntity = Data.FirstOrDefault(x => x.Id == entity.Id); + if(storedEntity == null) + throw new Exception($"Entity with id [{entity.Id}] not found"); + + Data.Remove(storedEntity); + return Task.CompletedTask; + } } } \ No newline at end of file diff --git a/Homeworks/Base/src/PromoCodeFactory.WebHost.Tests/EmployeeApiTests.cs b/Homeworks/Base/src/PromoCodeFactory.WebHost.Tests/EmployeeApiTests.cs new file mode 100644 index 00000000..ffee7702 --- /dev/null +++ b/Homeworks/Base/src/PromoCodeFactory.WebHost.Tests/EmployeeApiTests.cs @@ -0,0 +1,196 @@ +using System.Net; +using System.Net.Http.Json; +using PromoCodeFactory.DataAccess.Data; +using PromoCodeFactory.WebHost.Models; + +namespace PromoCodeFactory.WebHost.Tests; + +public class EmployeeApiTests(WebHostFixture factory) : IClassFixture +{ + private readonly HttpClient _client = factory.CreateClient(); + + [Fact] + public void FakeData_Contains_Two_Employees() + { + Assert.Equal(2, FakeDataFactory.Employees.Count); + } + + [Fact] + public void FakeData_Contains_Two_Roles() + { + Assert.Equal(2, FakeDataFactory.Roles.Count); + } + + [Fact] + public async Task EmployeeCreation_Succeed() + { + var employeeRequest = new EmployeeCreationRequest + { + FirstName = "John", + LastName = "Doe", + Email = "jd@ya.ru" + }; + + var response = await _client.PostAsJsonAsync(factory.ApiRoot + "Employees", employeeRequest); + + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + + var content = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(content); + Assert.NotEqual(Guid.Empty, content.Id); + Assert.Equal(employeeRequest.Email, content.Email); + Assert.Equal(employeeRequest.FirstName, content.FirstName); + Assert.Equal(employeeRequest.LastName, content.LastName); + } + + [Fact] + public async Task Employee_Fails_On_Wrong_Body() + { + var response = await _client.PostAsJsonAsync(factory.ApiRoot + "Employees", + new + { + FullName = "John Doe", + Email = "mymail" + }); + + Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); + } + + [Fact] + public async Task Employee_Persists_Between_Calls() + { + var employeeRequest = new EmployeeCreationRequest + { + FirstName = "John", + LastName = "Doe", + Email = "jd@ya.ru" + }; + + var response = await _client.PostAsJsonAsync($"{factory.ApiRoot}Employees", employeeRequest); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + var createdEmployee = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(createdEmployee); + Assert.NotEqual(Guid.Empty, createdEmployee.Id); + + response = await _client.GetAsync($"{factory.ApiRoot}Employees/{createdEmployee.Id:D}"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var fetchedEmployee = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(fetchedEmployee); + Assert.Equal(createdEmployee.Id, fetchedEmployee.Id); + Assert.Equal(createdEmployee.Email, fetchedEmployee.Email); + Assert.Equal(createdEmployee.FirstName, fetchedEmployee.FirstName); + Assert.Equal(createdEmployee.LastName, fetchedEmployee.LastName); + Assert.Equivalent(createdEmployee.Roles, fetchedEmployee.Roles); + Assert.Equal(createdEmployee.AppliedPromocodesCount, fetchedEmployee.AppliedPromocodesCount); + } + + [Fact] + public async Task Employee_Get_Fails_On_Wrong_Id() + { + var response = await _client.GetAsync($"{factory.ApiRoot}Employees/{Guid.NewGuid():D}"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + private async Task GetFirstEmployee() + { + var response = await _client.GetAsync($"{factory.ApiRoot}Employees"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var employees = await response.Content.ReadFromJsonAsync>(); + Assert.NotNull(employees); + var employeeShort = employees.FirstOrDefault(); + Assert.NotNull(employeeShort); + + var employeeResponse = await _client.GetAsync($"{factory.ApiRoot}Employees/{employeeShort.Id:D}"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var employeeFull = await employeeResponse.Content.ReadFromJsonAsync(); + Assert.NotNull(employeeFull); + + return employeeFull; + } + + [Fact] + public async Task Employee_Get_Without_Id_Returns_Collection() + { + var response = await _client.GetAsync($"{factory.ApiRoot}Employees"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var employees = await response.Content.ReadFromJsonAsync>(); + Assert.NotNull(employees); + Assert.NotEmpty(employees); + } + + [Fact] + public async Task Employee_Get_Employee_On_Right_Id() + { + var sourceEmployee = await GetFirstEmployee(); + + var response = await _client.GetAsync($"{factory.ApiRoot}Employees/{sourceEmployee.Id:D}"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var employee = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(employee); + Assert.Equal(sourceEmployee.Id, employee.Id); + Assert.Equal(sourceEmployee.Email, employee.Email); + Assert.Equal(sourceEmployee.FirstName, employee.FirstName); + Assert.Equal(sourceEmployee.LastName, employee.LastName); + Assert.Equal(sourceEmployee.Roles.Count, employee.Roles.Count); + Assert.Equivalent(sourceEmployee.Roles, employee.Roles); + } + + [Fact] + public async Task Employee_Updates_Existing_Employee() + { + var sourceEmployee = await GetFirstEmployee(); + Assert.NotNull(sourceEmployee); + + sourceEmployee.Email += "+"; + sourceEmployee.FirstName += "+"; + sourceEmployee.LastName += "+"; + + var response = await _client.PutAsJsonAsync($"{factory.ApiRoot}Employees/{sourceEmployee.Id:D}", sourceEmployee); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var updatedEmployee = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(updatedEmployee); + Assert.Equal(updatedEmployee.Email, sourceEmployee.Email); + Assert.Equal(updatedEmployee.FirstName, sourceEmployee.FirstName); + Assert.Equal(updatedEmployee.LastName, sourceEmployee.LastName); + } + + + [Fact] + public async Task Employee_Delete_Existing_Employee_Returns_OK() + { + var sourceEmployee = await GetFirstEmployee(); + Assert.NotNull(sourceEmployee); + + var response = await _client.DeleteAsync($"{factory.ApiRoot}Employees/{sourceEmployee.Id:D}"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + } + + [Fact] + public async Task Employee_Delete_Missing_Employee_Returns_404() + { + var response = await _client.DeleteAsync($"{factory.ApiRoot}Employees/{Guid.NewGuid():D}"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Employee_Delete_Existing_Employee_Removes_Employee() + { + var sourceEmployee = await GetFirstEmployee(); + Assert.NotNull(sourceEmployee); + + var response = await _client.DeleteAsync($"{factory.ApiRoot}Employees/{sourceEmployee.Id:D}"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + response = await _client.GetAsync($"{factory.ApiRoot}Employees/{sourceEmployee.Id:D}"); + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + + } + +} \ No newline at end of file diff --git a/Homeworks/Base/src/PromoCodeFactory.WebHost.Tests/PromoCodeFactory.WebHost.Tests.csproj b/Homeworks/Base/src/PromoCodeFactory.WebHost.Tests/PromoCodeFactory.WebHost.Tests.csproj new file mode 100644 index 00000000..07ae5698 --- /dev/null +++ b/Homeworks/Base/src/PromoCodeFactory.WebHost.Tests/PromoCodeFactory.WebHost.Tests.csproj @@ -0,0 +1,27 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + diff --git a/Homeworks/Base/src/PromoCodeFactory.WebHost.Tests/WebHostFixture.cs b/Homeworks/Base/src/PromoCodeFactory.WebHost.Tests/WebHostFixture.cs new file mode 100644 index 00000000..77029096 --- /dev/null +++ b/Homeworks/Base/src/PromoCodeFactory.WebHost.Tests/WebHostFixture.cs @@ -0,0 +1,9 @@ +using Microsoft.AspNetCore.Mvc.Testing; + +namespace PromoCodeFactory.WebHost.Tests; + +public class WebHostFixture() : WebApplicationFactory +{ + public string ApiRoot = "/api/v1/"; + +} \ No newline at end of file diff --git a/Homeworks/Base/src/PromoCodeFactory.WebHost/Controllers/EmployeesController.cs b/Homeworks/Base/src/PromoCodeFactory.WebHost/Controllers/EmployeesController.cs index df1c41dd..37708963 100644 --- a/Homeworks/Base/src/PromoCodeFactory.WebHost/Controllers/EmployeesController.cs +++ b/Homeworks/Base/src/PromoCodeFactory.WebHost/Controllers/EmployeesController.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using PromoCodeFactory.Core.Abstractions.Repositories; using PromoCodeFactory.Core.Domain.Administration; +using PromoCodeFactory.WebHost.Mappers; using PromoCodeFactory.WebHost.Models; namespace PromoCodeFactory.WebHost.Controllers @@ -14,15 +16,12 @@ namespace PromoCodeFactory.WebHost.Controllers /// [ApiController] [Route("api/v1/[controller]")] - public class EmployeesController : ControllerBase + public class EmployeesController( + IRepository employeeRepository, + IEmployeeMappers employeeMappers + ) + : ControllerBase { - private readonly IRepository _employeeRepository; - - public EmployeesController(IRepository employeeRepository) - { - _employeeRepository = employeeRepository; - } - /// /// Получить данные всех сотрудников /// @@ -30,17 +29,8 @@ public EmployeesController(IRepository employeeRepository) [HttpGet] public async Task> GetEmployeesAsync() { - var employees = await _employeeRepository.GetAllAsync(); - - var employeesModelList = employees.Select(x => - new EmployeeShortResponse() - { - Id = x.Id, - Email = x.Email, - FullName = x.FullName, - }).ToList(); - - return employeesModelList; + var employees = await employeeRepository.GetAllAsync(); + return employees.Select(x => employeeMappers.EmployeeToEmployeeShortResponse.Map(x)).ToList(); } /// @@ -50,25 +40,70 @@ public async Task> GetEmployeesAsync() [HttpGet("{id:guid}")] public async Task> GetEmployeeByIdAsync(Guid id) { - var employee = await _employeeRepository.GetByIdAsync(id); + var employee = await employeeRepository.GetByIdAsync(id); if (employee == null) return NotFound(); - var employeeModel = new EmployeeResponse() - { - Id = employee.Id, - Email = employee.Email, - Roles = employee.Roles.Select(x => new RoleItemResponse() - { - Name = x.Name, - Description = x.Description - }).ToList(), - FullName = employee.FullName, - AppliedPromocodesCount = employee.AppliedPromocodesCount - }; - + var employeeModel = employeeMappers.EmployeeToEmployeeResponse.Map(employee); return employeeModel; } + + /// + /// Создать нового сотрудника + /// + /// Данные для создания сотрудника + /// + [HttpPost] + public async Task> CreateEmployee(EmployeeCreationRequest creationRequest) + { + if (creationRequest.Email == null + || creationRequest.FirstName == null + || creationRequest.LastName == null) + return BadRequest(); + + var employee = employeeMappers.EmployeeCreationRequestToEmployee.Map(creationRequest); + await employeeRepository.AddAsync(employee); + + var savedEmployee = await employeeRepository.GetByIdAsync(employee.Id); + return savedEmployee != null + ? Created($"/api/v1//employees/{employee.Id:D}", employeeMappers.EmployeeToEmployeeResponse.Map(employee)) + : StatusCode((int)HttpStatusCode.InternalServerError); + } + + /// + /// Удаление сотрудника по его идентификатору + /// + /// Идентификатор пользователя + /// + [HttpDelete("{id:guid}")] + public async Task> DeleteEmployee(Guid id) + { + var employee = await employeeRepository.GetByIdAsync(id); + if (employee == null) + return NotFound(id); + + await employeeRepository.DeleteAsync(employee); + return Ok(id); + } + + /// + /// Обновление данных сотрудника + /// + /// + /// + /// + [HttpPut("{id:guid}")] + public async Task> UpdateEmployee(Guid id, EmployeeUpdateRequest updateRequest) + { + var employee = employeeMappers.EmployeeUpdateRequestToEmployee.Map(updateRequest); + employee.Id = id; + + var updatedEmployee = await employeeRepository.UpdateAsync(employee); + return Ok(employeeMappers.EmployeeToEmployeeResponse.Map(updatedEmployee)); + } } + + + } \ No newline at end of file diff --git a/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/EmployeeMappers/EmployeeCreationRequestToEmployeeMapper.cs b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/EmployeeMappers/EmployeeCreationRequestToEmployeeMapper.cs new file mode 100644 index 00000000..45968d97 --- /dev/null +++ b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/EmployeeMappers/EmployeeCreationRequestToEmployeeMapper.cs @@ -0,0 +1,21 @@ +using System; +using PromoCodeFactory.WebHost.Models; + +namespace PromoCodeFactory.WebHost.Mappers.EmployeeMappers; + +public class EmployeeCreationRequestToEmployeeMapper : IMapper +{ + /// + public Core.Domain.Administration.Employee Map(EmployeeCreationRequest source) + { + return new Core.Domain.Administration.Employee + { + Id = Guid.NewGuid(), + FirstName = source.FirstName, + LastName = source.LastName, + Email = source.Email, + Roles = [], + AppliedPromocodesCount = 0 + }; + } +} \ No newline at end of file diff --git a/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/EmployeeMappers/EmployeeMappers.cs b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/EmployeeMappers/EmployeeMappers.cs new file mode 100644 index 00000000..581af6e7 --- /dev/null +++ b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/EmployeeMappers/EmployeeMappers.cs @@ -0,0 +1,25 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using PromoCodeFactory.Core.Domain.Administration; +using PromoCodeFactory.WebHost.Models; + +namespace PromoCodeFactory.WebHost.Mappers.EmployeeMappers; + +public class EmployeeMappers(IServiceProvider services) : IEmployeeMappers +{ + /// + public IMapper EmployeeToEmployeeResponse { get; } + = services.GetRequiredService>(); + + /// + public IMapper EmployeeToEmployeeShortResponse { get; } + = services.GetRequiredService>(); + + /// + public IMapper EmployeeCreationRequestToEmployee { get; } + = services.GetRequiredService>(); + + /// + public IMapper EmployeeUpdateRequestToEmployee { get; } + = services.GetRequiredService>(); +} \ No newline at end of file diff --git a/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/EmployeeMappers/EmployeeToEmployeeResponseMapper.cs b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/EmployeeMappers/EmployeeToEmployeeResponseMapper.cs new file mode 100644 index 00000000..e5ac2127 --- /dev/null +++ b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/EmployeeMappers/EmployeeToEmployeeResponseMapper.cs @@ -0,0 +1,21 @@ +using System.Linq; +using PromoCodeFactory.WebHost.Models; + +namespace PromoCodeFactory.WebHost.Mappers.EmployeeMappers; + +public class EmployeeToEmployeeResponseMapper(IRoleMappers roleMappers) : IMapper +{ + /// + public EmployeeResponse Map(Core.Domain.Administration.Employee source) + { + return new EmployeeResponse + { + Id = source.Id, + FirstName = source.FirstName, + LastName = source.LastName, + Email = source.Email, + Roles = source.Roles?.Select(roleMappers.RoleToRoleItemResponse.Map).ToList() ?? [], + AppliedPromocodesCount = source.AppliedPromocodesCount + }; + } +} \ No newline at end of file diff --git a/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/EmployeeMappers/EmployeeToEmployeeShortResponseMapper.cs b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/EmployeeMappers/EmployeeToEmployeeShortResponseMapper.cs new file mode 100644 index 00000000..060791c2 --- /dev/null +++ b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/EmployeeMappers/EmployeeToEmployeeShortResponseMapper.cs @@ -0,0 +1,17 @@ +using PromoCodeFactory.WebHost.Models; + +namespace PromoCodeFactory.WebHost.Mappers.EmployeeMappers; + +public class EmployeeToEmployeeShortResponseMapper : IMapper +{ + /// + public EmployeeShortResponse Map(Core.Domain.Administration.Employee source) + { + return new EmployeeShortResponse + { + Id = source.Id, + FullName = $"{source.FirstName} {source.LastName}", + Email = source.Email + }; + } +} \ No newline at end of file diff --git a/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/EmployeeMappers/EmployeeUpdateRequestToEmployeeMapper.cs b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/EmployeeMappers/EmployeeUpdateRequestToEmployeeMapper.cs new file mode 100644 index 00000000..eae2625c --- /dev/null +++ b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/EmployeeMappers/EmployeeUpdateRequestToEmployeeMapper.cs @@ -0,0 +1,18 @@ +using PromoCodeFactory.WebHost.Models; +using PromoCodeFactory.Core.Domain.Administration; + +namespace PromoCodeFactory.WebHost.Mappers.EmployeeMappers; + +public class EmployeeUpdateRequestToEmployeeMapper : IMapper +{ + /// + public Employee Map(EmployeeUpdateRequest source) + { + return new Employee + { + FirstName = source.FirstName, + LastName = source.LastName, + Email = source.Email + }; + } +} \ No newline at end of file diff --git a/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/IEmployeeMappers.cs b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/IEmployeeMappers.cs new file mode 100644 index 00000000..ad9c407b --- /dev/null +++ b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/IEmployeeMappers.cs @@ -0,0 +1,12 @@ +using PromoCodeFactory.Core.Domain.Administration; +using PromoCodeFactory.WebHost.Models; + +namespace PromoCodeFactory.WebHost.Mappers; + +public interface IEmployeeMappers +{ + IMapper EmployeeToEmployeeResponse { get; } + IMapper EmployeeToEmployeeShortResponse { get; } + IMapper EmployeeCreationRequestToEmployee { get; } + IMapper EmployeeUpdateRequestToEmployee { get; } +} \ No newline at end of file diff --git a/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/IMapper.cs b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/IMapper.cs new file mode 100644 index 00000000..d8363d8d --- /dev/null +++ b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/IMapper.cs @@ -0,0 +1,6 @@ +namespace PromoCodeFactory.WebHost.Mappers; + +public interface IMapper +{ + TDestination Map(TSource source); +} \ No newline at end of file diff --git a/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/IRoleMappers.cs b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/IRoleMappers.cs new file mode 100644 index 00000000..c5c5bd34 --- /dev/null +++ b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/IRoleMappers.cs @@ -0,0 +1,10 @@ +using PromoCodeFactory.Core.Domain.Administration; +using PromoCodeFactory.WebHost.Models; + +namespace PromoCodeFactory.WebHost.Mappers; + +public interface IRoleMappers +{ + IMapper RoleToRoleItemResponse { get; } + +} \ No newline at end of file diff --git a/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/RoleMappers/RoleItemToRoleResponseMapper.cs b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/RoleMappers/RoleItemToRoleResponseMapper.cs new file mode 100644 index 00000000..2e3e5b97 --- /dev/null +++ b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/RoleMappers/RoleItemToRoleResponseMapper.cs @@ -0,0 +1,18 @@ +using PromoCodeFactory.Core.Domain.Administration; +using PromoCodeFactory.WebHost.Models; + +namespace PromoCodeFactory.WebHost.Mappers.RoleMappers; + +public class RoleItemToRoleResponseMapper : IMapper +{ + /// + public Role Map(RoleItemResponse source) + { + return new Role + { + Id = source.Id, + Name = source.Name, + Description = source.Description + }; + } +} \ No newline at end of file diff --git a/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/RoleMappers/RoleMappers.cs b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/RoleMappers/RoleMappers.cs new file mode 100644 index 00000000..74247956 --- /dev/null +++ b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/RoleMappers/RoleMappers.cs @@ -0,0 +1,13 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using PromoCodeFactory.Core.Domain.Administration; +using PromoCodeFactory.WebHost.Models; + +namespace PromoCodeFactory.WebHost.Mappers.RoleMappers; + +public class RoleMappers(IServiceProvider services) : IRoleMappers +{ + /// + public IMapper RoleToRoleItemResponse { get; } + = services.GetRequiredService>(); +} \ No newline at end of file diff --git a/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/RoleMappers/RoleToRoleItemResponseMapper.cs b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/RoleMappers/RoleToRoleItemResponseMapper.cs new file mode 100644 index 00000000..04ba9505 --- /dev/null +++ b/Homeworks/Base/src/PromoCodeFactory.WebHost/Mappers/RoleMappers/RoleToRoleItemResponseMapper.cs @@ -0,0 +1,18 @@ +using PromoCodeFactory.Core.Domain.Administration; +using PromoCodeFactory.WebHost.Models; + +namespace PromoCodeFactory.WebHost.Mappers.RoleMappers; + +public class RoleToRoleItemResponseMapper : IMapper +{ + /// + public RoleItemResponse Map(Role source) + { + return new RoleItemResponse + { + Id = source.Id, + Name = source.Name, + Description = source.Description + }; + } +} \ No newline at end of file diff --git a/Homeworks/Base/src/PromoCodeFactory.WebHost/Models/EmployeeRequest.cs b/Homeworks/Base/src/PromoCodeFactory.WebHost/Models/EmployeeRequest.cs new file mode 100644 index 00000000..f5637c05 --- /dev/null +++ b/Homeworks/Base/src/PromoCodeFactory.WebHost/Models/EmployeeRequest.cs @@ -0,0 +1,18 @@ +namespace PromoCodeFactory.WebHost.Models; + +public abstract record EmployeeRequest +{ + public string FirstName { get; init; } + public string LastName { get; init; } + public string Email { get; init; } +} + +/// +/// Запрос на создание нового пользователя +/// +public record EmployeeCreationRequest : EmployeeRequest; + +/// +/// Запрос на изменение пользователя +/// +public record EmployeeUpdateRequest : EmployeeRequest; \ No newline at end of file diff --git a/Homeworks/Base/src/PromoCodeFactory.WebHost/Models/EmployeeResponse.cs b/Homeworks/Base/src/PromoCodeFactory.WebHost/Models/EmployeeResponse.cs index 85a5f807..1879731d 100644 --- a/Homeworks/Base/src/PromoCodeFactory.WebHost/Models/EmployeeResponse.cs +++ b/Homeworks/Base/src/PromoCodeFactory.WebHost/Models/EmployeeResponse.cs @@ -7,7 +7,15 @@ public class EmployeeResponse { public Guid Id { get; set; } - public string FullName { get; set; } + // FullName разделён на FirstName и LastName. + // Использование полного имени вместо частей делает + // невозможным корректное обновление из фронтенда + // (FullName на бэке надо разделить, но не ясно делить + // трёх- и более сегментные ФИО: Пхе Мун Сон, Эльчин Тахир оглы и пр.) + // public string FullName { get; set; } + public string FirstName { get; set; } + + public string LastName { get; set; } public string Email { get; set; } diff --git a/Homeworks/Base/src/PromoCodeFactory.WebHost/Startup.cs b/Homeworks/Base/src/PromoCodeFactory.WebHost/Startup.cs index 8151aa4a..a7ac0925 100644 --- a/Homeworks/Base/src/PromoCodeFactory.WebHost/Startup.cs +++ b/Homeworks/Base/src/PromoCodeFactory.WebHost/Startup.cs @@ -6,6 +6,10 @@ using PromoCodeFactory.Core.Domain.Administration; using PromoCodeFactory.DataAccess.Data; using PromoCodeFactory.DataAccess.Repositories; +using PromoCodeFactory.WebHost.Mappers; +using PromoCodeFactory.WebHost.Mappers.EmployeeMappers; +using PromoCodeFactory.WebHost.Mappers.RoleMappers; +using PromoCodeFactory.WebHost.Models; namespace PromoCodeFactory.WebHost { @@ -14,9 +18,9 @@ public class Startup public void ConfigureServices(IServiceCollection services) { services.AddControllers(); - services.AddSingleton(typeof(IRepository), (x) => + services.AddSingleton(typeof(IRepository), _ => new InMemoryRepository(FakeDataFactory.Employees)); - services.AddSingleton(typeof(IRepository), (x) => + services.AddSingleton(typeof(IRepository), _ => new InMemoryRepository(FakeDataFactory.Roles)); services.AddOpenApiDocument(options => @@ -24,6 +28,16 @@ public void ConfigureServices(IServiceCollection services) options.Title = "PromoCode Factory API Doc"; options.Version = "1.0"; }); + + services.AddSingleton, RoleToRoleItemResponseMapper>(); + services.AddSingleton, RoleItemToRoleResponseMapper>(); + services.AddSingleton(); + + services.AddSingleton, EmployeeToEmployeeResponseMapper>(); + services.AddSingleton, EmployeeToEmployeeShortResponseMapper>(); + services.AddSingleton, EmployeeCreationRequestToEmployeeMapper>(); + services.AddSingleton, EmployeeUpdateRequestToEmployeeMapper>(); + services.AddSingleton(); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) diff --git a/Homeworks/Base/src/PromoCodeFactory.sln b/Homeworks/Base/src/PromoCodeFactory.sln index 14085bb4..a1dba621 100644 --- a/Homeworks/Base/src/PromoCodeFactory.sln +++ b/Homeworks/Base/src/PromoCodeFactory.sln @@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PromoCodeFactory.DataAccess EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PromoCodeFactory.Core", "PromoCodeFactory.Core\PromoCodeFactory.Core.csproj", "{FA64A24E-D404-4E7D-89A8-B87E134F697F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PromoCodeFactory.WebHost.Tests", "PromoCodeFactory.WebHost.Tests\PromoCodeFactory.WebHost.Tests.csproj", "{6BA6AF5B-E700-49F2-9DC5-E1471EA632D4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {FA64A24E-D404-4E7D-89A8-B87E134F697F}.Debug|Any CPU.Build.0 = Debug|Any CPU {FA64A24E-D404-4E7D-89A8-B87E134F697F}.Release|Any CPU.ActiveCfg = Release|Any CPU {FA64A24E-D404-4E7D-89A8-B87E134F697F}.Release|Any CPU.Build.0 = Release|Any CPU + {6BA6AF5B-E700-49F2-9DC5-E1471EA632D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6BA6AF5B-E700-49F2-9DC5-E1471EA632D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6BA6AF5B-E700-49F2-9DC5-E1471EA632D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6BA6AF5B-E700-49F2-9DC5-E1471EA632D4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Homeworks/EF/src/PromoCodeFactory.Core/Abstractions/Repositories/IRepository.cs b/Homeworks/EF/src/PromoCodeFactory.Core/Abstractions/Repositories/IRepository.cs index a1eb21fe..e18840d4 100644 --- a/Homeworks/EF/src/PromoCodeFactory.Core/Abstractions/Repositories/IRepository.cs +++ b/Homeworks/EF/src/PromoCodeFactory.Core/Abstractions/Repositories/IRepository.cs @@ -10,6 +10,9 @@ public interface IRepository { Task> GetAllAsync(); - Task GetByIdAsync(Guid id); + Task GetByIdAsync(Guid id); + Task AddAsync(T customer); + Task UpdateAsync(T customer); + Task RemoveAsync(T customer); } } \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodeFactory.Core/Domain/Administration/Employee.cs b/Homeworks/EF/src/PromoCodeFactory.Core/Domain/Administration/Employee.cs index 28099d61..8ee1473d 100644 --- a/Homeworks/EF/src/PromoCodeFactory.Core/Domain/Administration/Employee.cs +++ b/Homeworks/EF/src/PromoCodeFactory.Core/Domain/Administration/Employee.cs @@ -1,17 +1,24 @@ using PromoCodeFactory.Core.Domain; using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace PromoCodeFactory.Core.Domain.Administration { public class Employee : BaseEntity { + [StringLength(50)] public string FirstName { get; set; } + + [StringLength(50)] public string LastName { get; set; } - + + [NotMapped] public string FullName => $"{FirstName} {LastName}"; + [StringLength(50)] public string Email { get; set; } public Role Role { get; set; } diff --git a/Homeworks/EF/src/PromoCodeFactory.Core/Domain/Administration/Role.cs b/Homeworks/EF/src/PromoCodeFactory.Core/Domain/Administration/Role.cs index f0fadc21..8eeff651 100644 --- a/Homeworks/EF/src/PromoCodeFactory.Core/Domain/Administration/Role.cs +++ b/Homeworks/EF/src/PromoCodeFactory.Core/Domain/Administration/Role.cs @@ -1,13 +1,19 @@ using PromoCodeFactory.Core.Domain; using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; namespace PromoCodeFactory.Core.Domain.Administration { public class Role : BaseEntity { + [StringLength(50)] public string Name { get; set; } + [StringLength(150)] public string Description { get; set; } + + public ICollection Employees { get; set; } } } \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodeFactory.Core/Domain/PromoCodeManagement/Customer.cs b/Homeworks/EF/src/PromoCodeFactory.Core/Domain/PromoCodeManagement/Customer.cs index 6f5c3593..eafc0087 100644 --- a/Homeworks/EF/src/PromoCodeFactory.Core/Domain/PromoCodeManagement/Customer.cs +++ b/Homeworks/EF/src/PromoCodeFactory.Core/Domain/PromoCodeManagement/Customer.cs @@ -1,19 +1,29 @@ using PromoCodeFactory.Core.Domain; -using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace PromoCodeFactory.Core.Domain.PromoCodeManagement { public class Customer : BaseEntity { + [StringLength(50)] public string FirstName { get; set; } + + [StringLength(50)] public string LastName { get; set; } + [NotMapped] public string FullName => $"{FirstName} {LastName}"; + [StringLength(50)] public string Email { get; set; } + + [StringLength(512)] + public string? Address { get; set; } - //TODO: Списки Preferences и Promocodes + public ICollection Preferences { get; set; } = new List(); + public ICollection PromoCodes { get; set; } = new List(); } } \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodeFactory.Core/Domain/PromoCodeManagement/CustomerPreference.cs b/Homeworks/EF/src/PromoCodeFactory.Core/Domain/PromoCodeManagement/CustomerPreference.cs new file mode 100644 index 00000000..b29ddb82 --- /dev/null +++ b/Homeworks/EF/src/PromoCodeFactory.Core/Domain/PromoCodeManagement/CustomerPreference.cs @@ -0,0 +1,9 @@ +using System; + +namespace PromoCodeFactory.Core.Domain.PromoCodeManagement; + +public class CustomerPreference : BaseEntity +{ + public Guid CustomersId { get; set; } + public Guid PreferencesId { get; set; } +} \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodeFactory.Core/Domain/PromoCodeManagement/CustomerPromoCode.cs b/Homeworks/EF/src/PromoCodeFactory.Core/Domain/PromoCodeManagement/CustomerPromoCode.cs new file mode 100644 index 00000000..6ef7475f --- /dev/null +++ b/Homeworks/EF/src/PromoCodeFactory.Core/Domain/PromoCodeManagement/CustomerPromoCode.cs @@ -0,0 +1,9 @@ +using System; + +namespace PromoCodeFactory.Core.Domain.PromoCodeManagement; + +public class CustomerPromoCode : BaseEntity +{ + public Guid CustomersId { get; set; } + public Guid PromoCodesId { get; set; } +} \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodeFactory.Core/Domain/PromoCodeManagement/Preference.cs b/Homeworks/EF/src/PromoCodeFactory.Core/Domain/PromoCodeManagement/Preference.cs index 28e172a4..d8a22d06 100644 --- a/Homeworks/EF/src/PromoCodeFactory.Core/Domain/PromoCodeManagement/Preference.cs +++ b/Homeworks/EF/src/PromoCodeFactory.Core/Domain/PromoCodeManagement/Preference.cs @@ -1,8 +1,17 @@ -namespace PromoCodeFactory.Core.Domain.PromoCodeManagement +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace PromoCodeFactory.Core.Domain.PromoCodeManagement { public class Preference : BaseEntity { + [StringLength(50)] public string Name { get; set; } + + [StringLength(150)] + public string Value { get; set; } + + public ICollection Customers { get; set; } = new List(); } } \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodeFactory.Core/Domain/PromoCodeManagement/PromoCode.cs b/Homeworks/EF/src/PromoCodeFactory.Core/Domain/PromoCodeManagement/PromoCode.cs index 3305a41a..52415e45 100644 --- a/Homeworks/EF/src/PromoCodeFactory.Core/Domain/PromoCodeManagement/PromoCode.cs +++ b/Homeworks/EF/src/PromoCodeFactory.Core/Domain/PromoCodeManagement/PromoCode.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Runtime; using PromoCodeFactory.Core.Domain; using PromoCodeFactory.Core.Domain.Administration; @@ -8,18 +10,23 @@ namespace PromoCodeFactory.Core.Domain.PromoCodeManagement public class PromoCode : BaseEntity { + [StringLength(20)] public string Code { get; set; } + [StringLength(150)] public string ServiceInfo { get; set; } public DateTime BeginDate { get; set; } public DateTime EndDate { get; set; } + [StringLength(150)] public string PartnerName { get; set; } public Employee PartnerManager { get; set; } public Preference Preference { get; set; } + + public ICollection Customers { get; set; } } } \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodeFactory.Core/PromoCodeFactory.Core.csproj b/Homeworks/EF/src/PromoCodeFactory.Core/PromoCodeFactory.Core.csproj index 4c096661..b8bbd025 100644 --- a/Homeworks/EF/src/PromoCodeFactory.Core/PromoCodeFactory.Core.csproj +++ b/Homeworks/EF/src/PromoCodeFactory.Core/PromoCodeFactory.Core.csproj @@ -2,6 +2,7 @@ net8.0 + enable diff --git a/Homeworks/EF/src/PromoCodeFactory.DataAccess/Data/FakeDataFactory.cs b/Homeworks/EF/src/PromoCodeFactory.DataAccess/Data/FakeDataFactory.cs index aa665794..4901e49e 100644 --- a/Homeworks/EF/src/PromoCodeFactory.DataAccess/Data/FakeDataFactory.cs +++ b/Homeworks/EF/src/PromoCodeFactory.DataAccess/Data/FakeDataFactory.cs @@ -52,16 +52,19 @@ public static class FakeDataFactory { Id = Guid.Parse("ef7f299f-92d7-459f-896e-078ed53ef99c"), Name = "Театр", + Value = "theatre" }, new Preference() { Id = Guid.Parse("c4bda62e-fc74-4256-a956-4760b3858cbd"), Name = "Семья", + Value = "family" }, new Preference() { Id = Guid.Parse("76324c47-68d2-472d-abb8-33cfa8cc0c84"), Name = "Дети", + Value = "children" } }; @@ -78,7 +81,7 @@ public static IEnumerable Customers Email = "ivan_sergeev@mail.ru", FirstName = "Иван", LastName = "Петров", - //TODO: Добавить предзаполненный список предпочтений + Preferences = Preferences.Where(x => x.Name == "Театр" || x.Name == "Семья").ToList() } }; diff --git a/Homeworks/EF/src/PromoCodeFactory.DataAccess/EfContext.cs b/Homeworks/EF/src/PromoCodeFactory.DataAccess/EfContext.cs new file mode 100644 index 00000000..25973feb --- /dev/null +++ b/Homeworks/EF/src/PromoCodeFactory.DataAccess/EfContext.cs @@ -0,0 +1,106 @@ +using System.Linq; +using Microsoft.EntityFrameworkCore; +using PromoCodeFactory.Core.Domain.Administration; +using PromoCodeFactory.Core.Domain.PromoCodeManagement; +using PromoCodeFactory.DataAccess.Data; + +namespace PromoCodeFactory.DataAccess; + +public class EfContext(DbContextOptions options) : DbContext(options) +{ + + public static void SeedData(DbContext context) + { + context.Set().AddRange(FakeDataFactory.Roles); + context.Set().AddRange(FakeDataFactory.Preferences); + context.SaveChanges(); + + var customers = FakeDataFactory.Customers.ToList(); + foreach (var customer in customers) + { + customer.Preferences = + context + .Set() + .AsEnumerable() + .Where(r => customer.Preferences.Any(c => c.Id == r.Id)) + .ToList(); + } + context.Set().AddRange(customers); + + var employees = FakeDataFactory.Employees.ToList(); + foreach (var employee in employees) + { + employee.Role = context.Set().AsEnumerable().Single(r => r.Id == employee.Role.Id); + } + context.Set().AddRange(employees); + + context.SaveChanges(); + } + + /// + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + //NOTE: MaxLength для строк указана через атрибуты в сущностях + modelBuilder + .Entity() + .HasKey(x => x.Id); + + modelBuilder + .Entity() + .HasKey(x => x.Id); + + modelBuilder + .Entity() + .HasOne(x => x.Preference); + + modelBuilder + .Entity() + .HasOne(x => x.Role) + .WithMany(x => x.Employees); + + modelBuilder + .Entity() + .Navigation(x => x.Role) + .AutoInclude(); + + modelBuilder + .Entity() + .HasMany(x => x.Preferences) + .WithMany(x => x.Customers) + .UsingEntity( + nameof(CustomerPreference), + l => l.HasOne(typeof(Customer)).WithMany().HasForeignKey("CustomersId").IsRequired(), + r => r.HasOne(typeof(Preference)).WithMany().HasForeignKey("PreferencesId").IsRequired(), + j => j.HasKey("CustomersId", "PreferencesId") + ); + + modelBuilder + .Entity() + .HasMany(x => x.PromoCodes) + .WithMany(x => x.Customers) + .UsingEntity( + nameof(CustomerPromoCode), + l => l.HasOne(typeof(Customer)).WithMany().HasForeignKey("CustomersId").IsRequired(), + r => r.HasOne(typeof(PromoCode)).WithMany().HasForeignKey("PromoCodesId").IsRequired(), + j => j.HasKey("CustomersId", "PromoCodesId") + ); + + // modelBuilder + // .Entity() + // .HasMany(x => x.PromoCodes) + // .WithOne(x => x.Customer) + // .OnDelete(DeleteBehavior.Cascade); + + modelBuilder + .Entity() + .Navigation(x => x.PromoCodes) + .AutoInclude(); + + modelBuilder + .Entity() + .Navigation(x => x.Preferences) + .AutoInclude(); + + base.OnModelCreating(modelBuilder); + } +} \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodeFactory.DataAccess/Migrations/20251002171936_Initial.Designer.cs b/Homeworks/EF/src/PromoCodeFactory.DataAccess/Migrations/20251002171936_Initial.Designer.cs new file mode 100644 index 00000000..1d71cacc --- /dev/null +++ b/Homeworks/EF/src/PromoCodeFactory.DataAccess/Migrations/20251002171936_Initial.Designer.cs @@ -0,0 +1,264 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PromoCodeFactory.DataAccess; + +#nullable disable + +namespace PromoCodeFactory.DataAccess.Migrations +{ + [DbContext(typeof(EfContext))] + [Migration("20251002171936_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.9"); + + modelBuilder.Entity("CustomerPreference", b => + { + b.Property("CustomersId") + .HasColumnType("TEXT"); + + b.Property("PreferencesId") + .HasColumnType("TEXT"); + + b.HasKey("CustomersId", "PreferencesId"); + + b.HasIndex("PreferencesId"); + + b.ToTable("CustomerPreference"); + }); + + modelBuilder.Entity("CustomerPromoCode", b => + { + b.Property("CustomersId") + .HasColumnType("TEXT"); + + b.Property("PromoCodesId") + .HasColumnType("TEXT"); + + b.HasKey("CustomersId", "PromoCodesId"); + + b.HasIndex("PromoCodesId"); + + b.ToTable("CustomerPromoCode"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.Administration.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AppliedPromocodesCount") + .HasColumnType("INTEGER"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("Employee"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.Administration.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Role"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.PromoCodeManagement.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Customer"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.PromoCodeManagement.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Preference"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.PromoCodeManagement.PromoCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BeginDate") + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("PartnerManagerId") + .HasColumnType("TEXT"); + + b.Property("PartnerName") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("PreferenceId") + .HasColumnType("TEXT"); + + b.Property("ServiceInfo") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PartnerManagerId"); + + b.HasIndex("PreferenceId"); + + b.ToTable("PromoCode"); + }); + + modelBuilder.Entity("CustomerPreference", b => + { + b.HasOne("PromoCodeFactory.Core.Domain.PromoCodeManagement.Customer", null) + .WithMany() + .HasForeignKey("CustomersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PromoCodeFactory.Core.Domain.PromoCodeManagement.Preference", null) + .WithMany() + .HasForeignKey("PreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CustomerPromoCode", b => + { + b.HasOne("PromoCodeFactory.Core.Domain.PromoCodeManagement.Customer", null) + .WithMany() + .HasForeignKey("CustomersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PromoCodeFactory.Core.Domain.PromoCodeManagement.PromoCode", null) + .WithMany() + .HasForeignKey("PromoCodesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.Administration.Employee", b => + { + b.HasOne("PromoCodeFactory.Core.Domain.Administration.Role", "Role") + .WithMany("Employees") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.PromoCodeManagement.PromoCode", b => + { + b.HasOne("PromoCodeFactory.Core.Domain.Administration.Employee", "PartnerManager") + .WithMany() + .HasForeignKey("PartnerManagerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PromoCodeFactory.Core.Domain.PromoCodeManagement.Preference", "Preference") + .WithMany() + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PartnerManager"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.Administration.Role", b => + { + b.Navigation("Employees"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Homeworks/EF/src/PromoCodeFactory.DataAccess/Migrations/20251002171936_Initial.cs b/Homeworks/EF/src/PromoCodeFactory.DataAccess/Migrations/20251002171936_Initial.cs new file mode 100644 index 00000000..fcc14c6d --- /dev/null +++ b/Homeworks/EF/src/PromoCodeFactory.DataAccess/Migrations/20251002171936_Initial.cs @@ -0,0 +1,205 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PromoCodeFactory.DataAccess.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Customer", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + FirstName = table.Column(type: "TEXT", maxLength: 50, nullable: false), + LastName = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Email = table.Column(type: "TEXT", maxLength: 50, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Customer", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Preference", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Value = table.Column(type: "TEXT", maxLength: 150, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Preference", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Role", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 150, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Role", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "CustomerPreference", + columns: table => new + { + CustomersId = table.Column(type: "TEXT", nullable: false), + PreferencesId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CustomerPreference", x => new { x.CustomersId, x.PreferencesId }); + table.ForeignKey( + name: "FK_CustomerPreference_Customer_CustomersId", + column: x => x.CustomersId, + principalTable: "Customer", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_CustomerPreference_Preference_PreferencesId", + column: x => x.PreferencesId, + principalTable: "Preference", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "Employee", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + FirstName = table.Column(type: "TEXT", maxLength: 50, nullable: false), + LastName = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Email = table.Column(type: "TEXT", maxLength: 50, nullable: false), + RoleId = table.Column(type: "TEXT", nullable: false), + AppliedPromocodesCount = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Employee", x => x.Id); + table.ForeignKey( + name: "FK_Employee_Role_RoleId", + column: x => x.RoleId, + principalTable: "Role", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "PromoCode", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Code = table.Column(type: "TEXT", maxLength: 20, nullable: false), + ServiceInfo = table.Column(type: "TEXT", maxLength: 150, nullable: false), + BeginDate = table.Column(type: "TEXT", nullable: false), + EndDate = table.Column(type: "TEXT", nullable: false), + PartnerName = table.Column(type: "TEXT", maxLength: 150, nullable: false), + PartnerManagerId = table.Column(type: "TEXT", nullable: false), + PreferenceId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PromoCode", x => x.Id); + table.ForeignKey( + name: "FK_PromoCode_Employee_PartnerManagerId", + column: x => x.PartnerManagerId, + principalTable: "Employee", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_PromoCode_Preference_PreferenceId", + column: x => x.PreferenceId, + principalTable: "Preference", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "CustomerPromoCode", + columns: table => new + { + CustomersId = table.Column(type: "TEXT", nullable: false), + PromoCodesId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CustomerPromoCode", x => new { x.CustomersId, x.PromoCodesId }); + table.ForeignKey( + name: "FK_CustomerPromoCode_Customer_CustomersId", + column: x => x.CustomersId, + principalTable: "Customer", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_CustomerPromoCode_PromoCode_PromoCodesId", + column: x => x.PromoCodesId, + principalTable: "PromoCode", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_CustomerPreference_PreferencesId", + table: "CustomerPreference", + column: "PreferencesId"); + + migrationBuilder.CreateIndex( + name: "IX_CustomerPromoCode_PromoCodesId", + table: "CustomerPromoCode", + column: "PromoCodesId"); + + migrationBuilder.CreateIndex( + name: "IX_Employee_RoleId", + table: "Employee", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "IX_PromoCode_PartnerManagerId", + table: "PromoCode", + column: "PartnerManagerId"); + + migrationBuilder.CreateIndex( + name: "IX_PromoCode_PreferenceId", + table: "PromoCode", + column: "PreferenceId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CustomerPreference"); + + migrationBuilder.DropTable( + name: "CustomerPromoCode"); + + migrationBuilder.DropTable( + name: "Customer"); + + migrationBuilder.DropTable( + name: "PromoCode"); + + migrationBuilder.DropTable( + name: "Employee"); + + migrationBuilder.DropTable( + name: "Preference"); + + migrationBuilder.DropTable( + name: "Role"); + } + } +} diff --git a/Homeworks/EF/src/PromoCodeFactory.DataAccess/Migrations/20251002172008_CustomerAddress.Designer.cs b/Homeworks/EF/src/PromoCodeFactory.DataAccess/Migrations/20251002172008_CustomerAddress.Designer.cs new file mode 100644 index 00000000..8548e91b --- /dev/null +++ b/Homeworks/EF/src/PromoCodeFactory.DataAccess/Migrations/20251002172008_CustomerAddress.Designer.cs @@ -0,0 +1,268 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PromoCodeFactory.DataAccess; + +#nullable disable + +namespace PromoCodeFactory.DataAccess.Migrations +{ + [DbContext(typeof(EfContext))] + [Migration("20251002172008_CustomerAddress")] + partial class CustomerAddress + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.9"); + + modelBuilder.Entity("CustomerPreference", b => + { + b.Property("CustomersId") + .HasColumnType("TEXT"); + + b.Property("PreferencesId") + .HasColumnType("TEXT"); + + b.HasKey("CustomersId", "PreferencesId"); + + b.HasIndex("PreferencesId"); + + b.ToTable("CustomerPreference"); + }); + + modelBuilder.Entity("CustomerPromoCode", b => + { + b.Property("CustomersId") + .HasColumnType("TEXT"); + + b.Property("PromoCodesId") + .HasColumnType("TEXT"); + + b.HasKey("CustomersId", "PromoCodesId"); + + b.HasIndex("PromoCodesId"); + + b.ToTable("CustomerPromoCode"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.Administration.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AppliedPromocodesCount") + .HasColumnType("INTEGER"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("Employee"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.Administration.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Role"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.PromoCodeManagement.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Address") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Customer"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.PromoCodeManagement.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Preference"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.PromoCodeManagement.PromoCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BeginDate") + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("PartnerManagerId") + .HasColumnType("TEXT"); + + b.Property("PartnerName") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("PreferenceId") + .HasColumnType("TEXT"); + + b.Property("ServiceInfo") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PartnerManagerId"); + + b.HasIndex("PreferenceId"); + + b.ToTable("PromoCode"); + }); + + modelBuilder.Entity("CustomerPreference", b => + { + b.HasOne("PromoCodeFactory.Core.Domain.PromoCodeManagement.Customer", null) + .WithMany() + .HasForeignKey("CustomersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PromoCodeFactory.Core.Domain.PromoCodeManagement.Preference", null) + .WithMany() + .HasForeignKey("PreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CustomerPromoCode", b => + { + b.HasOne("PromoCodeFactory.Core.Domain.PromoCodeManagement.Customer", null) + .WithMany() + .HasForeignKey("CustomersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PromoCodeFactory.Core.Domain.PromoCodeManagement.PromoCode", null) + .WithMany() + .HasForeignKey("PromoCodesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.Administration.Employee", b => + { + b.HasOne("PromoCodeFactory.Core.Domain.Administration.Role", "Role") + .WithMany("Employees") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.PromoCodeManagement.PromoCode", b => + { + b.HasOne("PromoCodeFactory.Core.Domain.Administration.Employee", "PartnerManager") + .WithMany() + .HasForeignKey("PartnerManagerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PromoCodeFactory.Core.Domain.PromoCodeManagement.Preference", "Preference") + .WithMany() + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PartnerManager"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.Administration.Role", b => + { + b.Navigation("Employees"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Homeworks/EF/src/PromoCodeFactory.DataAccess/Migrations/20251002172008_CustomerAddress.cs b/Homeworks/EF/src/PromoCodeFactory.DataAccess/Migrations/20251002172008_CustomerAddress.cs new file mode 100644 index 00000000..64bf4527 --- /dev/null +++ b/Homeworks/EF/src/PromoCodeFactory.DataAccess/Migrations/20251002172008_CustomerAddress.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace PromoCodeFactory.DataAccess.Migrations +{ + /// + public partial class CustomerAddress : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Address", + table: "Customer", + type: "TEXT", + maxLength: 512, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Address", + table: "Customer"); + } + } +} diff --git a/Homeworks/EF/src/PromoCodeFactory.DataAccess/Migrations/EfContextModelSnapshot.cs b/Homeworks/EF/src/PromoCodeFactory.DataAccess/Migrations/EfContextModelSnapshot.cs new file mode 100644 index 00000000..b27d54fa --- /dev/null +++ b/Homeworks/EF/src/PromoCodeFactory.DataAccess/Migrations/EfContextModelSnapshot.cs @@ -0,0 +1,265 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using PromoCodeFactory.DataAccess; + +#nullable disable + +namespace PromoCodeFactory.DataAccess.Migrations +{ + [DbContext(typeof(EfContext))] + partial class EfContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.9"); + + modelBuilder.Entity("CustomerPreference", b => + { + b.Property("CustomersId") + .HasColumnType("TEXT"); + + b.Property("PreferencesId") + .HasColumnType("TEXT"); + + b.HasKey("CustomersId", "PreferencesId"); + + b.HasIndex("PreferencesId"); + + b.ToTable("CustomerPreference"); + }); + + modelBuilder.Entity("CustomerPromoCode", b => + { + b.Property("CustomersId") + .HasColumnType("TEXT"); + + b.Property("PromoCodesId") + .HasColumnType("TEXT"); + + b.HasKey("CustomersId", "PromoCodesId"); + + b.HasIndex("PromoCodesId"); + + b.ToTable("CustomerPromoCode"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.Administration.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AppliedPromocodesCount") + .HasColumnType("INTEGER"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("Employee"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.Administration.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Role"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.PromoCodeManagement.Customer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Address") + .HasMaxLength(512) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Customer"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.PromoCodeManagement.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Preference"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.PromoCodeManagement.PromoCode", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BeginDate") + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("PartnerManagerId") + .HasColumnType("TEXT"); + + b.Property("PartnerName") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("PreferenceId") + .HasColumnType("TEXT"); + + b.Property("ServiceInfo") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PartnerManagerId"); + + b.HasIndex("PreferenceId"); + + b.ToTable("PromoCode"); + }); + + modelBuilder.Entity("CustomerPreference", b => + { + b.HasOne("PromoCodeFactory.Core.Domain.PromoCodeManagement.Customer", null) + .WithMany() + .HasForeignKey("CustomersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PromoCodeFactory.Core.Domain.PromoCodeManagement.Preference", null) + .WithMany() + .HasForeignKey("PreferencesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("CustomerPromoCode", b => + { + b.HasOne("PromoCodeFactory.Core.Domain.PromoCodeManagement.Customer", null) + .WithMany() + .HasForeignKey("CustomersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PromoCodeFactory.Core.Domain.PromoCodeManagement.PromoCode", null) + .WithMany() + .HasForeignKey("PromoCodesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.Administration.Employee", b => + { + b.HasOne("PromoCodeFactory.Core.Domain.Administration.Role", "Role") + .WithMany("Employees") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.PromoCodeManagement.PromoCode", b => + { + b.HasOne("PromoCodeFactory.Core.Domain.Administration.Employee", "PartnerManager") + .WithMany() + .HasForeignKey("PartnerManagerId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("PromoCodeFactory.Core.Domain.PromoCodeManagement.Preference", "Preference") + .WithMany() + .HasForeignKey("PreferenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PartnerManager"); + + b.Navigation("Preference"); + }); + + modelBuilder.Entity("PromoCodeFactory.Core.Domain.Administration.Role", b => + { + b.Navigation("Employees"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Homeworks/EF/src/PromoCodeFactory.DataAccess/PromoCodeFactory.DataAccess.csproj b/Homeworks/EF/src/PromoCodeFactory.DataAccess/PromoCodeFactory.DataAccess.csproj index 4b290698..e4f6d03b 100644 --- a/Homeworks/EF/src/PromoCodeFactory.DataAccess/PromoCodeFactory.DataAccess.csproj +++ b/Homeworks/EF/src/PromoCodeFactory.DataAccess/PromoCodeFactory.DataAccess.csproj @@ -2,10 +2,20 @@ net8.0 + enable + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/Homeworks/EF/src/PromoCodeFactory.DataAccess/Repositories/EfRepository.cs b/Homeworks/EF/src/PromoCodeFactory.DataAccess/Repositories/EfRepository.cs new file mode 100644 index 00000000..8deae478 --- /dev/null +++ b/Homeworks/EF/src/PromoCodeFactory.DataAccess/Repositories/EfRepository.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using PromoCodeFactory.Core.Abstractions.Repositories; +using PromoCodeFactory.Core.Domain; + +namespace PromoCodeFactory.DataAccess.Repositories; + +public class EfRepository(EfContext context) : IRepository + where T : BaseEntity +{ + /// + public async Task> GetAllAsync() + { + var result = await context.Set().ToListAsync(); + return result; + } + + /// + public async Task GetByIdAsync(Guid id) + { + var result = await context.Set().SingleOrDefaultAsync(x => x.Id == id); + return result; + } + + /// + public async Task AddAsync(T customer) + { + var presentCustomer = await context.Set().FirstOrDefaultAsync(x => x.Id == customer.Id); + if(presentCustomer != null) + throw new Exception("Customer already exists"); + + var result = await context.AddAsync(customer); + await context.SaveChangesAsync(); + return result.Entity; + } + + /// + public async Task UpdateAsync(T entity) + { + var result = context.Set().Update(entity); + await context.SaveChangesAsync(); + return result.Entity; + } + + /// + public async Task RemoveAsync(T entity) + { + context.Set().Remove(entity); + await context.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodeFactory.DataAccess/Repositories/InMemoryRepository.cs b/Homeworks/EF/src/PromoCodeFactory.DataAccess/Repositories/InMemoryRepository.cs index dd099f5e..d99eac24 100644 --- a/Homeworks/EF/src/PromoCodeFactory.DataAccess/Repositories/InMemoryRepository.cs +++ b/Homeworks/EF/src/PromoCodeFactory.DataAccess/Repositories/InMemoryRepository.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using PromoCodeFactory.Core.Abstractions.Repositories; using PromoCodeFactory.Core.Domain; +using PromoCodeFactory.Core.Domain.PromoCodeManagement; namespace PromoCodeFactory.DataAccess.Repositories { @@ -23,9 +24,27 @@ public Task> GetAllAsync() return Task.FromResult(Data); } - public Task GetByIdAsync(Guid id) + public Task GetByIdAsync(Guid id) { return Task.FromResult(Data.FirstOrDefault(x => x.Id == id)); } + + /// + public Task AddAsync(T customer) + { + throw new NotImplementedException(); + } + + /// + public Task UpdateAsync(T customer) + { + throw new NotImplementedException(); + } + + /// + public Task RemoveAsync(T customer) + { + throw new NotImplementedException(); + } } } \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodeFactory.WebHost/Controllers/CustomersController.cs b/Homeworks/EF/src/PromoCodeFactory.WebHost/Controllers/CustomersController.cs index cf069a09..3b6e9c5d 100644 --- a/Homeworks/EF/src/PromoCodeFactory.WebHost/Controllers/CustomersController.cs +++ b/Homeworks/EF/src/PromoCodeFactory.WebHost/Controllers/CustomersController.cs @@ -1,7 +1,14 @@ using Microsoft.AspNetCore.Mvc; using PromoCodeFactory.WebHost.Models; using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using PromoCodeFactory.Core.Abstractions.Repositories; +using PromoCodeFactory.Core.Domain.PromoCodeManagement; namespace PromoCodeFactory.WebHost.Controllers { @@ -10,42 +17,198 @@ namespace PromoCodeFactory.WebHost.Controllers /// [ApiController] [Route("api/v1/[controller]")] - public class CustomersController - : ControllerBase + public class CustomersController( + ILogger logger, + IRepository customersRepository, + IRepository preferenceRepository + ) : ControllerBase { + /// + /// Получение списка клиентов + /// + /// + /// Возвращает полный список клиентов, зарегистрированных в БД + /// + /// Список из [HttpGet] - public Task> GetCustomersAsync() + public async Task>> GetCustomersAsync() { - //TODO: Добавить получение списка клиентов - throw new NotImplementedException(); + try + { + var customers = await customersRepository.GetAllAsync(); + return Ok(customers.Select(x => new CustomerShortResponse + { + Id = x.Id, + Email = x.Email, + FirstName = x.FirstName, + LastName = x.LastName + }).ToList()); + } + catch (Exception e) + { + logger.LogError(e, "Failed to get list of customers: [{msg}]", e.Message); + return StatusCode(500, e.Message); + } } - [HttpGet("{id}")] - public Task> GetCustomerAsync(Guid id) + /// + /// Данные о клиенте + /// + /// + /// Возвращает полные данные + /// + /// Идентификатор клиента + /// Полные данные пользователя + [HttpGet("{id:guid}")] + public async Task> GetCustomerAsync(Guid id) { - //TODO: Добавить получение клиента вместе с выданными ему промомкодами - throw new NotImplementedException(); + try + { + var customer = await customersRepository.GetByIdAsync(id); + if (customer == null) + return NotFound(); + + return Ok(CreateCustomerResponse(customer)); + } + catch (Exception e) + { + logger.LogError(e, "Error get customer with id [{id:D}]: {msg}", id, e.Message); + return StatusCode(StatusCodes.Status500InternalServerError, e.Message); + } } + /// + /// Создание нового клиента + /// + /// Данные создаваемого клиента + /// Код 201 при успешном создании клиента, 500 в случае ошибки [HttpPost] - public Task CreateCustomerAsync(CreateOrEditCustomerRequest request) + public async Task CreateCustomerAsync(CreateOrEditCustomerRequest request) { - //TODO: Добавить создание нового клиента вместе с его предпочтениями - throw new NotImplementedException(); + try + { + var customer = new Customer + { + Id = Guid.NewGuid(), + Email = request.Email, + FirstName = request.FirstName, + LastName = request.LastName, + Preferences = await GetPreferences(request.PreferenceIds), + PromoCodes = [] + }; + + await customersRepository.AddAsync(customer); + return Created( + HttpContext.Request.Path.Add(new PathString($"/{customer.Id:D}")), + CreateCustomerResponse(customer) + ); + } + catch (KeyNotFoundException e) + { + logger.LogError(e, "Bad request on customer creation: {msg}", e.Message); + return BadRequest(e.Message); + } + catch (Exception e) + { + logger.LogError(e, "Error on customer creation: {msg}", e.Message); + return StatusCode(500, e.Message); + } } - [HttpPut("{id}")] - public Task EditCustomersAsync(Guid id, CreateOrEditCustomerRequest request) + /// + /// Обновление данных клиента + /// + /// Идентификатор клиента + /// Новые данные пользователя + /// + [HttpPut("{id:guid}")] + public async Task EditCustomersAsync(Guid id, CreateOrEditCustomerRequest request) { - //TODO: Обновить данные клиента вместе с его предпочтениями - throw new NotImplementedException(); + try + { + var customer = await customersRepository.GetByIdAsync(id); + if (customer == null) + return NotFound(id); + + customer.Email = request.Email; + customer.FirstName = request.FirstName; + customer.LastName = request.LastName; + + customer.Preferences = await GetPreferences(request.PreferenceIds); + + await customersRepository.UpdateAsync(customer); + return Ok(CreateCustomerResponse(customer)); + } + catch (KeyNotFoundException e) + { + logger.LogError(e, "Bad request on customer update: {msg}", e.Message); + return BadRequest(e.Message); + } + catch (Exception e) + { + logger.LogError(e, "Error on customer update: {msg}", e.Message); + return StatusCode(500, e.Message); + } } + /// + /// Удаление клиента по его идентификатору + /// + /// Идентификатор клиента + /// 200 при успешном удалении, 500 при ошибке, 404 если клиент не найден [HttpDelete] - public Task DeleteCustomer(Guid id) + public async Task DeleteCustomer(Guid id) + { + try + { + var customer = await customersRepository.GetByIdAsync(id); + if (customer == null) + return NotFound(id); + + await customersRepository.RemoveAsync(customer); + return Ok(); + } + catch (Exception e) + { + logger.LogError(e, "Error on customer deletion: {msg}", e.Message); + return StatusCode(500, e.Message); + } + } + + private async Task> GetPreferences(IEnumerable preferenceIds) { - //TODO: Удаление клиента вместе с выданными ему промокодами - throw new NotImplementedException(); + var preferences = new List(); + foreach (var id in preferenceIds) + { + var preference = await preferenceRepository.GetByIdAsync(id); + if (preference == null) + throw new KeyNotFoundException($"Preference with id [{id}] was not found"); + preferences.Add(preference); + } + + return preferences; + } + + private static CustomerResponse CreateCustomerResponse(Customer customerEntity) + { + return new CustomerResponse + { + Id = customerEntity.Id, + Email = customerEntity.Email, + FirstName = customerEntity.FirstName, + LastName = customerEntity.LastName, + PromoCodes = customerEntity.PromoCodes.Select(x => new PromoCodeShortResponse() + { + Id = x.Id, + BeginDate = x.BeginDate.ToString(CultureInfo.InvariantCulture), + EndDate = x.EndDate.ToString(CultureInfo.InvariantCulture), + Code = x.Code, + PartnerName = x.PartnerName, + ServiceInfo = x.ServiceInfo + }).ToList(), + + Preferences = customerEntity.Preferences.Select(x => x.Id).ToList() + }; } } } \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodeFactory.WebHost/Controllers/EmployeesController.cs b/Homeworks/EF/src/PromoCodeFactory.WebHost/Controllers/EmployeesController.cs index 2dd31dd2..7b5ac04f 100644 --- a/Homeworks/EF/src/PromoCodeFactory.WebHost/Controllers/EmployeesController.cs +++ b/Homeworks/EF/src/PromoCodeFactory.WebHost/Controllers/EmployeesController.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using PromoCodeFactory.Core.Abstractions.Repositories; using PromoCodeFactory.Core.Domain.Administration; using PromoCodeFactory.WebHost.Models; @@ -14,34 +15,35 @@ namespace PromoCodeFactory.WebHost.Controllers /// [ApiController] [Route("api/v1/[controller]")] - public class EmployeesController + public class EmployeesController(ILogger logger, IRepository employeeRepository) : ControllerBase { - private readonly IRepository _employeeRepository; - - public EmployeesController(IRepository employeeRepository) - { - _employeeRepository = employeeRepository; - } - /// /// Получить данные всех сотрудников /// /// [HttpGet] - public async Task> GetEmployeesAsync() + public async Task>> GetEmployeesAsync() { - var employees = await _employeeRepository.GetAllAsync(); + try + { + var employees = await employeeRepository.GetAllAsync(); - var employeesModelList = employees.Select(x => - new EmployeeShortResponse() - { - Id = x.Id, - Email = x.Email, - FullName = x.FullName, - }).ToList(); + var employeesModelList = employees.Select(x => + new EmployeeShortResponse() + { + Id = x.Id, + Email = x.Email, + FullName = x.FullName, + }).ToList(); - return employeesModelList; + return Ok(employeesModelList); + } + catch (Exception e) + { + logger.LogError(e, "Error get employees list: [{msg}]", e.Message); + return StatusCode(500, e.Message); + } } /// @@ -51,25 +53,33 @@ public async Task> GetEmployeesAsync() [HttpGet("{id:guid}")] public async Task> GetEmployeeByIdAsync(Guid id) { - var employee = await _employeeRepository.GetByIdAsync(id); + try + { + var employee = await employeeRepository.GetByIdAsync(id); - if (employee == null) - return NotFound(); + if (employee == null) + return NotFound(); - var employeeModel = new EmployeeResponse() - { - Id = employee.Id, - Email = employee.Email, - Role = new RoleItemResponse() + var employeeModel = new EmployeeResponse() { - Name = employee.Role.Name, - Description = employee.Role.Description - }, - FullName = employee.FullName, - AppliedPromocodesCount = employee.AppliedPromocodesCount - }; + Id = employee.Id, + Email = employee.Email, + Role = new RoleItemResponse() + { + Name = employee.Role.Name, + Description = employee.Role.Description + }, + FullName = employee.FullName, + AppliedPromocodesCount = employee.AppliedPromocodesCount + }; - return employeeModel; + return employeeModel; + } + catch (Exception e) + { + logger.LogError(e, "Error get employee with id [{id:D}] list: [{msg}]", id, e.Message); + return StatusCode(500, e.Message); + } } } } \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodeFactory.WebHost/Controllers/PreferencesController.cs b/Homeworks/EF/src/PromoCodeFactory.WebHost/Controllers/PreferencesController.cs new file mode 100644 index 00000000..29d6ccc2 --- /dev/null +++ b/Homeworks/EF/src/PromoCodeFactory.WebHost/Controllers/PreferencesController.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using PromoCodeFactory.Core.Abstractions.Repositories; +using PromoCodeFactory.Core.Domain.PromoCodeManagement; +using PromoCodeFactory.WebHost.Models; + +namespace PromoCodeFactory.WebHost.Controllers +{ + /// + /// Предпочтения + /// + [ApiController] + [Route("api/v1/[controller]")] + public class PreferencesController( + ILogger logger, + IRepository preferencesRepository + ) : ControllerBase + { + /// + /// Получить все предпочтения + /// + /// + [HttpGet] + public async Task>> GetPreferencesAsync() + { + try + { + var codes = await preferencesRepository.GetAllAsync(); + + return Ok(codes.Select(x => new PreferenceResponse() + { + Name = x.Name, + Value = x.Value + }).ToList()); + } + catch (Exception e) + { + logger.LogError(e, "Error get preferences list: [{msg}]", e.Message); + return StatusCode(500, e.Message); + } + } + } +} \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodeFactory.WebHost/Controllers/PromocodesController.cs b/Homeworks/EF/src/PromoCodeFactory.WebHost/Controllers/PromocodesController.cs index 5b96c327..2cea8a19 100644 --- a/Homeworks/EF/src/PromoCodeFactory.WebHost/Controllers/PromocodesController.cs +++ b/Homeworks/EF/src/PromoCodeFactory.WebHost/Controllers/PromocodesController.cs @@ -1,7 +1,13 @@ using System; using System.Collections.Generic; +using System.Globalization; +using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using PromoCodeFactory.Core.Abstractions.Repositories; +using PromoCodeFactory.Core.Domain.Administration; +using PromoCodeFactory.Core.Domain.PromoCodeManagement; using PromoCodeFactory.WebHost.Models; namespace PromoCodeFactory.WebHost.Controllers @@ -11,18 +17,40 @@ namespace PromoCodeFactory.WebHost.Controllers /// [ApiController] [Route("api/v1/[controller]")] - public class PromocodesController - : ControllerBase + public class PromocodesController( + ILogger logger, + IRepository codesRepository, + IRepository preferencesRepository, + IRepository customersRepository, + IRepository employeeRepository + ) : ControllerBase { /// /// Получить все промокоды /// /// [HttpGet] - public Task>> GetPromocodesAsync() + public async Task>> GetPromocodesAsync() { - //TODO: Получить все промокоды - throw new NotImplementedException(); + try + { + var codes = await codesRepository.GetAllAsync(); + + return Ok(codes.Select(x => new PromoCodeShortResponse + { + Id = x.Id, + Code = x.Code, + BeginDate = x.BeginDate.ToString(CultureInfo.InvariantCulture), + EndDate = x.EndDate.ToString(CultureInfo.InvariantCulture), + PartnerName = x.PartnerName, + ServiceInfo = x.ServiceInfo + }).ToList()); + } + catch (Exception e) + { + logger.LogError(e, "Error get promocodes list: [{msg}]", e.Message); + return StatusCode(500, e.Message); + } } /// @@ -30,10 +58,38 @@ public Task>> GetPromocodesAsync() /// /// [HttpPost] - public Task GivePromoCodesToCustomersWithPreferenceAsync(GivePromoCodeRequest request) + public async Task GivePromoCodesToCustomersWithPreferenceAsync(GivePromoCodeRequest request) { - //TODO: Создать промокод и выдать его клиентам с указанным предпочтением - throw new NotImplementedException(); + try + { + var preference = (await preferencesRepository.GetAllAsync()) + .FirstOrDefault(x => x.Name == request.Preference); + + if (preference == null) + return BadRequest("Preference not found"); + + var employees = await employeeRepository.GetAllAsync(); + var code = new PromoCode + { + Id = Guid.NewGuid(), + PartnerName = request.PartnerName, + ServiceInfo = request.ServiceInfo, + Code = request.PromoCode, + Preference = preference, + + BeginDate = DateTime.UtcNow, + Customers = preference.Customers.ToList(), + PartnerManager = employees.Single(x => x.FullName == request.PartnerName) + }; + + await codesRepository.AddAsync(code); + return Ok(); + } + catch (Exception e) + { + logger.LogError(e, "Error apply promocodes: [{msg}]", e.Message); + return StatusCode(500, e.Message); + } } } } \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodeFactory.WebHost/Controllers/RolesController.cs b/Homeworks/EF/src/PromoCodeFactory.WebHost/Controllers/RolesController.cs index 4de2687f..f432cc0b 100644 --- a/Homeworks/EF/src/PromoCodeFactory.WebHost/Controllers/RolesController.cs +++ b/Homeworks/EF/src/PromoCodeFactory.WebHost/Controllers/RolesController.cs @@ -1,7 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using PromoCodeFactory.Core.Abstractions.Repositories; using PromoCodeFactory.Core.Domain.Administration; using PromoCodeFactory.WebHost.Models; @@ -13,33 +15,37 @@ namespace PromoCodeFactory.WebHost.Controllers /// [ApiController] [Route("api/v1/[controller]")] - public class RolesController + public class RolesController( + ILogger logger, + IRepository rolesRepository + ) : ControllerBase { - private readonly IRepository _rolesRepository; - - public RolesController(IRepository rolesRepository) - { - _rolesRepository = rolesRepository; - } - /// /// Получить все доступные роли сотрудников /// /// [HttpGet] - public async Task> GetRolesAsync() + public async Task>> GetRolesAsync() { - var roles = await _rolesRepository.GetAllAsync(); + try + { + var roles = await rolesRepository.GetAllAsync(); - var rolesModelList = roles.Select(x => - new RoleItemResponse() - { - Id = x.Id, - Name = x.Name, - Description = x.Description - }).ToList(); + var rolesModelList = roles.Select(x => + new RoleItemResponse() + { + Id = x.Id, + Name = x.Name, + Description = x.Description + }).ToList(); - return rolesModelList; + return rolesModelList; + } + catch (Exception e) + { + logger.LogError(e, "Error get roles list: [{msg}]", e.Message); + return StatusCode(500, e.Message); + } } } } \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodeFactory.WebHost/Models/CreateOrEditCustomerRequest.cs b/Homeworks/EF/src/PromoCodeFactory.WebHost/Models/CreateOrEditCustomerRequest.cs index 59f313fb..2717ae69 100644 --- a/Homeworks/EF/src/PromoCodeFactory.WebHost/Models/CreateOrEditCustomerRequest.cs +++ b/Homeworks/EF/src/PromoCodeFactory.WebHost/Models/CreateOrEditCustomerRequest.cs @@ -1,13 +1,21 @@ using System; using System.Collections.Generic; +using System.Text.Json.Serialization; namespace PromoCodeFactory.WebHost.Models { public class CreateOrEditCustomerRequest { - public string FirstName { get; set; } - public string LastName { get; set; } - public string Email { get; set; } - public List PreferenceIds { get; set; } + [JsonInclude] + public string FirstName { get; set; } = string.Empty; + + [JsonInclude] + public string LastName { get; set; } = string.Empty; + + [JsonInclude] + public string Email { get; set; } = string.Empty; + + [JsonInclude] + public List PreferenceIds { get; set; } = []; } } \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodeFactory.WebHost/Models/CustomerResponse.cs b/Homeworks/EF/src/PromoCodeFactory.WebHost/Models/CustomerResponse.cs index 97ea3e6b..83899a74 100644 --- a/Homeworks/EF/src/PromoCodeFactory.WebHost/Models/CustomerResponse.cs +++ b/Homeworks/EF/src/PromoCodeFactory.WebHost/Models/CustomerResponse.cs @@ -6,10 +6,11 @@ namespace PromoCodeFactory.WebHost.Models public class CustomerResponse { public Guid Id { get; set; } - public string FirstName { get; set; } - public string LastName { get; set; } - public string Email { get; set; } + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; //TODO: Добавить список предпочтений - public List PromoCodes { get; set; } + public List PromoCodes { get; set; } = []; + public List Preferences { get; set; } = []; } } \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodeFactory.WebHost/Models/PreferenceResponse.cs b/Homeworks/EF/src/PromoCodeFactory.WebHost/Models/PreferenceResponse.cs new file mode 100644 index 00000000..d2777aa3 --- /dev/null +++ b/Homeworks/EF/src/PromoCodeFactory.WebHost/Models/PreferenceResponse.cs @@ -0,0 +1,12 @@ +using System; +using PromoCodeFactory.Core.Domain.PromoCodeManagement; + +namespace PromoCodeFactory.WebHost.Models +{ + public class PreferenceResponse + { + public string Name { get; set; } + + public string Value { get; set; } + } +} \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodeFactory.WebHost/PromoCodeFactory.WebHost.csproj b/Homeworks/EF/src/PromoCodeFactory.WebHost/PromoCodeFactory.WebHost.csproj index 4bb08b6e..0c6859e5 100644 --- a/Homeworks/EF/src/PromoCodeFactory.WebHost/PromoCodeFactory.WebHost.csproj +++ b/Homeworks/EF/src/PromoCodeFactory.WebHost/PromoCodeFactory.WebHost.csproj @@ -4,6 +4,7 @@ net8.0 true $(NoWarn);1591 + enable @@ -12,6 +13,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Homeworks/EF/src/PromoCodeFactory.WebHost/Startup.cs b/Homeworks/EF/src/PromoCodeFactory.WebHost/Startup.cs index 73a898fc..4b371790 100644 --- a/Homeworks/EF/src/PromoCodeFactory.WebHost/Startup.cs +++ b/Homeworks/EF/src/PromoCodeFactory.WebHost/Startup.cs @@ -1,11 +1,15 @@ +using System; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using PromoCodeFactory.Core.Abstractions.Repositories; using PromoCodeFactory.Core.Domain.Administration; using PromoCodeFactory.Core.Domain.PromoCodeManagement; -using PromoCodeFactory.DataAccess.Data; +using PromoCodeFactory.DataAccess; using PromoCodeFactory.DataAccess.Repositories; namespace PromoCodeFactory.WebHost @@ -17,15 +21,26 @@ public class Startup public void ConfigureServices(IServiceCollection services) { services.AddControllers(); - services.AddScoped(typeof(IRepository), (x) => - new InMemoryRepository(FakeDataFactory.Employees)); - services.AddScoped(typeof(IRepository), (x) => - new InMemoryRepository(FakeDataFactory.Roles)); - services.AddScoped(typeof(IRepository), (x) => - new InMemoryRepository(FakeDataFactory.Preferences)); - services.AddScoped(typeof(IRepository), (x) => - new InMemoryRepository(FakeDataFactory.Customers)); + + services.AddScoped, EfRepository>(); + services.AddScoped, EfRepository>(); + services.AddScoped, EfRepository>(); + services.AddScoped, EfRepository>(); + services.AddScoped, EfRepository>(); + services.AddDbContext(options => + { + options + .UseSqlite("Data Source=PromoCodeFactory.db") + .LogTo(Console.WriteLine, LogLevel.Trace) + .UseSeeding((c,b) => EfContext.SeedData(c)) + .UseAsyncSeeding((c,b, t) => + { + EfContext.SeedData(c); + return Task.CompletedTask; + }); + }); + services.AddOpenApiDocument(options => { options.Title = "PromoCode Factory API Doc"; @@ -34,11 +49,13 @@ public void ConfigureServices(IServiceCollection services) } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, EfContext efContext) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); + efContext.Database.EnsureDeleted(); + efContext.Database.EnsureCreated(); } else { diff --git a/Homeworks/EF/src/PromoCodeFactory.sln b/Homeworks/EF/src/PromoCodeFactory.sln index 39e21828..5deaad31 100644 --- a/Homeworks/EF/src/PromoCodeFactory.sln +++ b/Homeworks/EF/src/PromoCodeFactory.sln @@ -9,6 +9,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PromoCodeFactory.DataAccess EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PromoCodeFactory.Core", "PromoCodeFactory.Core\PromoCodeFactory.Core.csproj", "{FA64A24E-D404-4E7D-89A8-B87E134F697F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PromoCodesFactory.WebHost.Tests", "PromoCodesFactory.WebHost.Tests\PromoCodesFactory.WebHost.Tests.csproj", "{442B07B0-2981-43CB-80D6-4EEEA90A93F6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,6 +29,10 @@ Global {FA64A24E-D404-4E7D-89A8-B87E134F697F}.Debug|Any CPU.Build.0 = Debug|Any CPU {FA64A24E-D404-4E7D-89A8-B87E134F697F}.Release|Any CPU.ActiveCfg = Release|Any CPU {FA64A24E-D404-4E7D-89A8-B87E134F697F}.Release|Any CPU.Build.0 = Release|Any CPU + {442B07B0-2981-43CB-80D6-4EEEA90A93F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {442B07B0-2981-43CB-80D6-4EEEA90A93F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {442B07B0-2981-43CB-80D6-4EEEA90A93F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {442B07B0-2981-43CB-80D6-4EEEA90A93F6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Homeworks/EF/src/PromoCodesFactory.WebHost.Tests/CustomersControllerTests.cs b/Homeworks/EF/src/PromoCodesFactory.WebHost.Tests/CustomersControllerTests.cs new file mode 100644 index 00000000..fde9e805 --- /dev/null +++ b/Homeworks/EF/src/PromoCodesFactory.WebHost.Tests/CustomersControllerTests.cs @@ -0,0 +1,148 @@ +using System.Net.Http.Json; +using FluentAssertions; +using PromoCodeFactory.DataAccess.Data; +using PromoCodeFactory.WebHost.Models; + +namespace PromoCodesFactory.WebHost.Tests; + +public class CustomerControllerTests(WebHostFixture fixture) : IClassFixture +{ + + + [Fact] + public async Task Get_Should_Return_Status_200() + { + var client = fixture.GetClient(); + var response = await client.GetAsync("api/v1/Customers"); + response.Should().Be200Ok(); + } + + [Fact] + public async Task Get_Should_Return_List_With_One_Customer() + { + var client = fixture.GetClient(); + var response = await client.GetAsync("api/v1/Customers"); + response.Should().Be200Ok(); + + var content = (await response.Content.ReadFromJsonAsync>())?.ToArray(); + content.Should() + .NotBeNull() + .And.HaveCount(1) + .And.AllSatisfy(x => + { + x.Id.Should().Be(FakeDataFactory.Customers.First().Id); + x.Email.Should().Be(FakeDataFactory.Customers.First().Email); + x.FirstName.Should().Be(FakeDataFactory.Customers.First().FirstName); + x.LastName.Should().Be(FakeDataFactory.Customers.First().LastName); + } + ); + } + + [Fact] + public async Task Post_Should_Return_Status_201_On_Valid_Request() + { + var fakeCustomer = FakeDataFactory.Customers.First(); + var fakePreference = FakeDataFactory.Preferences.First(); + var client = fixture.GetClient(true); + var response = await client.PostAsJsonAsync("api/v1/Customers", + new CreateOrEditCustomerRequest + { + FirstName = fakeCustomer.FirstName, + LastName = fakeCustomer.LastName, + Email = fakeCustomer.Email, + PreferenceIds = [fakePreference.Id] + }); + response.Should().Be201Created(); + + var content = await response.Content.ReadFromJsonAsync(); + content.Should() + .NotBeNull() + .And.Satisfy(x => + { + x.Email.Should().Be(fakeCustomer.Email); + x.FirstName.Should().Be(fakeCustomer.FirstName); + x.LastName.Should().Be(fakeCustomer.LastName); + x.Preferences.Contains(fakePreference.Id).Should().BeTrue(); + } + ); + } + + [Fact] + public async Task Post_Should_Return_Status_400_On_Preference_Missing() + { + var fakeCustomer = FakeDataFactory.Customers.First(); + var client = fixture.GetClient(true); + var response = await client.PostAsJsonAsync("api/v1/Customers", + new CreateOrEditCustomerRequest + { + FirstName = fakeCustomer.FirstName, + LastName = fakeCustomer.LastName, + Email = fakeCustomer.Email, + PreferenceIds = [Guid.NewGuid()] + }); + response.Should().Be400BadRequest(); + } + + [Fact] + public async Task Put_Should_Return_Status_200_On_Valid_Request() + { + var fakeCustomer = FakeDataFactory.Customers.First(); + var fakePreference1 = FakeDataFactory.Preferences.First(); + var fakePreference2 = FakeDataFactory.Preferences.First(); + var client = fixture.GetClient(true); + var response = await client.PutAsJsonAsync($"api/v1/Customers/{fakeCustomer.Id:D}", + new CreateOrEditCustomerRequest + { + FirstName = fakeCustomer.FirstName, + LastName = fakeCustomer.LastName, + Email = fakeCustomer.Email, + PreferenceIds = [fakePreference1.Id, fakePreference2.Id] + }); + response.Should().Be200Ok(); + + var content = await response.Content.ReadFromJsonAsync(); + content.Should() + .NotBeNull() + .And.Satisfy(x => + { + x.Email.Should().Be(fakeCustomer.Email); + x.FirstName.Should().Be(fakeCustomer.FirstName); + x.LastName.Should().Be(fakeCustomer.LastName); + x.Preferences.Contains(fakePreference1.Id).Should().BeTrue(); + x.Preferences.Contains(fakePreference2.Id).Should().BeTrue(); + } + ); + } + + [Fact] + public async Task Put_Should_Return_Status_400_If_Preference_Not_Exists() + { + var fakeCustomer = FakeDataFactory.Customers.First(); + var client = fixture.GetClient(true); + var response = await client.PutAsJsonAsync($"api/v1/Customers/{fakeCustomer.Id:D}", + new CreateOrEditCustomerRequest + { + FirstName = fakeCustomer.FirstName, + LastName = fakeCustomer.LastName, + Email = fakeCustomer.Email, + PreferenceIds = [Guid.NewGuid()] + }); + response.Should().Be400BadRequest(); + } + + [Fact] + public async Task Put_Should_Return_Status_404_If_Customer_Not_Exists() + { + var fakeCustomer = FakeDataFactory.Customers.First(); + var client = fixture.GetClient(true); + var response = await client.PutAsJsonAsync($"api/v1/Customers/{Guid.NewGuid():D}", + new CreateOrEditCustomerRequest + { + FirstName = fakeCustomer.FirstName, + LastName = fakeCustomer.LastName, + Email = fakeCustomer.Email, + PreferenceIds = [Guid.NewGuid()] + }); + response.Should().Be404NotFound(); + } +} \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodesFactory.WebHost.Tests/PreferenceControllerTests.cs b/Homeworks/EF/src/PromoCodesFactory.WebHost.Tests/PreferenceControllerTests.cs new file mode 100644 index 00000000..dc36bc5b --- /dev/null +++ b/Homeworks/EF/src/PromoCodesFactory.WebHost.Tests/PreferenceControllerTests.cs @@ -0,0 +1,55 @@ +using System.Net.Http.Json; +using FluentAssertions; +using PromoCodeFactory.DataAccess.Data; +using PromoCodeFactory.WebHost.Models; + +namespace PromoCodesFactory.WebHost.Tests; + +public class PreferenceControllerTests(WebHostFixture fixture) : IClassFixture +{ + [Fact] + public async Task Get_Should_Return_Status_200() + { + var client = fixture.GetClient(); + var response = await client.GetAsync("api/v1/Preferences"); + response.Should().Be200Ok(); + } + + [Fact] + public async Task Get_Should_Return_List_With_One_Customer() + { + var client = fixture.GetClient(); + var response = await client.GetAsync("api/v1/Preferences"); + response.Should().Be200Ok(); + + var content = (await response.Content.ReadFromJsonAsync>())?.ToArray(); + content.Should() + .NotBeNull() + .And.HaveCount(FakeDataFactory.Preferences.Count()); + } + + [Fact] + public async Task Post_Should_Return_Status_405() + { + var client = fixture.GetClient(true); + var response = await client.PostAsJsonAsync("api/v1/Preferences", new { Test = "dummy" }); + response.Should().Be405MethodNotAllowed(); + } + + [Fact] + public async Task Put_Should_Return_Status_404() + { + var client = fixture.GetClient(true); + var response = await client.PutAsJsonAsync($"api/v1/Preferences/{Guid.NewGuid():D}", new { Test = "dummy" }); + response.Should().Be404NotFound(); + } + + + [Fact] + public async Task Delete_Should_Return_Status_404() + { + var client = fixture.GetClient(true); + var response = await client.DeleteAsync($"api/v1/Preferences/{Guid.NewGuid():D}"); + response.Should().Be404NotFound(); + } +} \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodesFactory.WebHost.Tests/PromoCodesFactory.WebHost.Tests.csproj b/Homeworks/EF/src/PromoCodesFactory.WebHost.Tests/PromoCodesFactory.WebHost.Tests.csproj new file mode 100644 index 00000000..1d2a09e4 --- /dev/null +++ b/Homeworks/EF/src/PromoCodesFactory.WebHost.Tests/PromoCodesFactory.WebHost.Tests.csproj @@ -0,0 +1,32 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Homeworks/EF/src/PromoCodesFactory.WebHost.Tests/PromocodesControllerTests.cs b/Homeworks/EF/src/PromoCodesFactory.WebHost.Tests/PromocodesControllerTests.cs new file mode 100644 index 00000000..e4808a39 --- /dev/null +++ b/Homeworks/EF/src/PromoCodesFactory.WebHost.Tests/PromocodesControllerTests.cs @@ -0,0 +1,105 @@ +using System.Net.Http.Json; +using FluentAssertions; +using PromoCodeFactory.DataAccess.Data; +using PromoCodeFactory.WebHost.Models; + +namespace PromoCodesFactory.WebHost.Tests; + +public class PromocodesControllerTests(WebHostFixture fixture) : IClassFixture +{ + [Fact] + public async Task Get_Should_Return_Status_200() + { + var client = fixture.GetClient(); + var response = await client.GetAsync("api/v1/Promocodes"); + response.Should().Be200Ok(); + } + + [Fact] + public async Task Get_Should_Return_Empty_List() + { + var client = fixture.GetClient(); + var response = await client.GetAsync("api/v1/Promocodes"); + response.Should().Be200Ok(); + + var content = (await response.Content.ReadFromJsonAsync>())?.ToArray(); + content.Should() + .NotBeNull() + .And.BeEmpty(); + } + + [Fact] + public async Task Post_Should_Add_Promocode_To_Customer() + { + var client = fixture.GetClient(true); + var response = await client.PostAsJsonAsync("api/v1/Promocodes", new GivePromoCodeRequest + { + PartnerName = FakeDataFactory.Employees.First().FullName, + ServiceInfo = "SvcInfo New", + Preference = "Театр", + PromoCode = "CODE_TEST" + }); + response.Should().Be200Ok(); + + var customerResponse = await client.GetAsync($"api/v1/Customers/{FakeDataFactory.Customers.First().Id}"); + customerResponse.Should().Be200Ok(); + + var customer = await customerResponse.Content.ReadFromJsonAsync(); + + customer.Should() + .NotBeNull() + .And.Satisfy(x => x.Preferences.Should().Contain(FakeDataFactory.Preferences.First().Id)); + + + } + + [Fact] + public async Task Post_Should_Not_Add_Promocode_To_Customer() + { + var client = fixture.GetClient(true); + var response = await client.PostAsJsonAsync("api/v1/Promocodes", new GivePromoCodeRequest + { + PartnerName = FakeDataFactory.Employees.First().FullName, + ServiceInfo = "SvcInfo New", + Preference = "Дети", + PromoCode = "CODE_TEST" + }); + response.Should().Be200Ok(); + + var customerResponse = await client.GetAsync($"api/v1/Customers/{FakeDataFactory.Customers.First().Id}"); + customerResponse.Should().Be200Ok(); + + var customer = await customerResponse.Content.ReadFromJsonAsync(); + + customer.Should() + .NotBeNull() + .And.Satisfy(x => x.Preferences.Should().NotContain(FakeDataFactory.Preferences.Last().Id)); + + + } + + [Fact] + public async Task Post_Should_Add_Customer_Promocode() + { + var client = fixture.GetClient(true); + var response = await client.PostAsJsonAsync("api/v1/Promocodes", new GivePromoCodeRequest + { + PartnerName = FakeDataFactory.Employees.First().FullName, + ServiceInfo = "SvcInfo New", + Preference = "Театр", + PromoCode = "CODE_TEST" + }); + response.Should().Be200Ok(); + + var customerResponse = await client.GetAsync($"api/v1/Customers/{FakeDataFactory.Customers.First().Id}"); + customerResponse.Should().Be200Ok(); + + var customer = await customerResponse.Content.ReadFromJsonAsync(); + + customer.Should() + .NotBeNull() + .And.Satisfy(x => x.Preferences.Should().Contain(FakeDataFactory.Preferences.First().Id)); + + + } +} \ No newline at end of file diff --git a/Homeworks/EF/src/PromoCodesFactory.WebHost.Tests/WebHostFixture.cs b/Homeworks/EF/src/PromoCodesFactory.WebHost.Tests/WebHostFixture.cs new file mode 100644 index 00000000..e5f6b74f --- /dev/null +++ b/Homeworks/EF/src/PromoCodesFactory.WebHost.Tests/WebHostFixture.cs @@ -0,0 +1,58 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.Extensions.DependencyInjection; +using PromoCodeFactory.Core.Abstractions.Repositories; +using PromoCodeFactory.Core.Domain.PromoCodeManagement; +using PromoCodeFactory.DataAccess; +using PromoCodeFactory.WebHost; + +namespace PromoCodesFactory.WebHost.Tests; + +public class WebHostFixture : WebApplicationFactory +{ + /// + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.UseEnvironment("Test"); + builder.ConfigureTestServices(services => + { + var descriptor = + services.SingleOrDefault(d => d.ServiceType == typeof(IDbContextOptionsConfiguration)); + if (descriptor != null) + services.Remove(descriptor); + + var id = Guid.NewGuid().ToString("D"); + + services.AddDbContext(options => + options.UseInMemoryDatabase(id) + .UseSeeding((c, _) => EfContext.SeedData(c)) + .UseAsyncSeeding((c, _, t) => + { + EfContext.SeedData(c); + return Task.CompletedTask; + })); + }); + base.ConfigureWebHost(builder); + } + + + internal HttpClient GetClient(bool standalone = false) + { + using var scope = Services.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); + + return CreateClient(); + } + + /// + protected override void Dispose(bool disposing) + { + Console.WriteLine("DISPOSE"); + base.Dispose(disposing); + } +} \ No newline at end of file