diff --git a/CarRental/CarRental/.github/workflows/dotnet-tests.yml b/CarRental/CarRental/.github/workflows/dotnet-tests.yml new file mode 100644 index 000000000..36c06b335 --- /dev/null +++ b/CarRental/CarRental/.github/workflows/dotnet-tests.yml @@ -0,0 +1,29 @@ +name: .NET Tests + +on: + push: + branches: [ main, lab_1 ] + pull_request: + branches: [ main, lab_1 ] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 8.0.x + + - name: Restore dependencies + run: dotnet restore ./CarRental/CarRental.slnx + + - name: Build + run: dotnet build ./CarRental/CarRental.slnx --no-restore --configuration Release + + - name: Test + run: dotnet test ./CarRental/CarRental.Tests/CarRental.Tests.csproj --configuration Release --verbosity normal diff --git a/CarRental/CarRental/CarRental.API/CarRental.API.csproj b/CarRental/CarRental/CarRental.API/CarRental.API.csproj new file mode 100644 index 000000000..2686cf64b --- /dev/null +++ b/CarRental/CarRental/CarRental.API/CarRental.API.csproj @@ -0,0 +1,25 @@ + + + net8.0 + enable + enable + true + $(NoWarn);1591 + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.API/Controllers/AnalyticsController.cs b/CarRental/CarRental/CarRental.API/Controllers/AnalyticsController.cs new file mode 100644 index 000000000..03e53cbba --- /dev/null +++ b/CarRental/CarRental/CarRental.API/Controllers/AnalyticsController.cs @@ -0,0 +1,199 @@ +using AutoMapper; +using CarRental.Application.Contracts.Dto; +using CarRental.Domain.Entities; +using CarRental.Domain.Interfaces; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace CarRental.Api.Controllers; + +/// +/// Контроллер для аналитических запросов и отчетов +/// +[ApiController] +[Route("api/analytics")] +public class AnalyticsController( + IRepository rentalsRepo, + IRepository carsRepo, + IRepository clientsRepo, + IRepository generationsRepo, + IMapper mapper) : ControllerBase +{ + /// + /// Получает список клиентов, арендовавших автомобили указанной модели, отсортированный по названию + /// + /// Название модели автомобиля + /// Список клиентов + [HttpGet("clients-by-model")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetClientsByModelSortedByName( + [FromQuery] string modelName) + { + var rentalsQuery = rentalsRepo.GetQueryable( + include: query => query + .Include(r => r.Car) + .ThenInclude(c => c.ModelGeneration) + .ThenInclude(mg => mg.Model) + .Include(r => r.Client)); + + var clients = await rentalsQuery + .Where(r => r.Car!.ModelGeneration!.Model!.Name == modelName) + .Select(r => r.Client) + .Distinct() + .OrderBy(c => c.FullName) + .ToListAsync(); + + var result = clients + .Select(mapper.Map) + .ToList(); + + return Ok(result); + } + + /// + /// Получает арендованные в данный момент автомобили + /// + /// Текущая дата проверки + /// Список арендованных автомобилей + [HttpGet("currently-rented-cars")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetCurrentlyRentedCars( + [FromQuery] DateTime currentDate) + { + var rentedCarIds = await rentalsRepo.GetQueryable() + .Where(r => r.RentalDate.AddHours(r.RentalHours) > currentDate) + .Select(r => r.CarId) + .Distinct() + .ToListAsync(); + + var rentedCars = await carsRepo.GetQueryable() + .Where(c => rentedCarIds.Contains(c.Id)) + .Include(c => c.ModelGeneration) + .ThenInclude(m => m!.Model) + .ToListAsync(); + + var result = rentedCars + .Select(mapper.Map) + .ToList(); + + return Ok(result); + } + + /// + /// Получает топ 5 самых популярных арендованных автомобилей + /// + /// Список автомобилей, которые можно взять напрокат + [HttpGet("top-5-most-rented-cars")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetTop5MostRentedCars() + { + var topCarStats = await rentalsRepo.GetQueryable() + .GroupBy(r => r.CarId) + .Select(g => new { CarId = g.Key, RentalCount = g.Count() }) + .OrderByDescending(x => x.RentalCount) + .Take(5) + .ToListAsync(); + + var topCarIds = topCarStats.Select(x => x.CarId).ToList(); + + var cars = await carsRepo.GetQueryable() + .Where(c => topCarIds.Contains(c.Id)) + .Include(c => c.ModelGeneration) + .ThenInclude(m => m!.Model) + .ToListAsync(); + + var carsDict = cars.ToDictionary(c => c.Id); + + var topCarsResult = topCarStats + .Where(x => carsDict.ContainsKey(x.CarId)) + .Select(x => new CarRentalCountDto( + mapper.Map(carsDict[x.CarId]), + x.RentalCount)) + .ToList(); + + return Ok(topCarsResult); + } + + /// + /// Получает количество арендованных автомобилей для каждого автомобиля + /// + /// Список всех автомобилей, которые были взяты в аренду + [HttpGet("rental-count-per-car")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetRentalCountPerCar() + { + var rentalCounts = await rentalsRepo.GetQueryable() + .GroupBy(r => r.CarId) + .Select(g => new { CarId = g.Key, Count = g.Count() }) + .ToDictionaryAsync(x => x.CarId, x => x.Count); + + var cars = await carsRepo.GetQueryable() + .Include(c => c.ModelGeneration) + .ThenInclude(m => m!.Model) + .ToListAsync(); + + var carsWithRentalCount = cars + .Select(car => new CarRentalCountDto( + mapper.Map(car), + rentalCounts.GetValueOrDefault(car.Id, 0))) + .OrderByDescending(x => x.RentalCount) + .ToList(); + + return Ok(carsWithRentalCount); + } + + /// + /// Получает топ 5 клиентов по общей сумме аренды + /// + /// Список клиентов с общей суммой арендной платы + [HttpGet("top-5-clients-by-rental-amount")] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetTop5ClientsByRentalAmount() + { + var rentals = await rentalsRepo.GetQueryable() + .Select(r => new { r.ClientId, r.CarId, r.RentalHours }) + .ToListAsync(); + + var cars = await carsRepo.GetQueryable() + .Select(c => new { c.Id, c.ModelGenerationId }) + .ToListAsync(); + + var generations = await generationsRepo.GetQueryable() + .Select(g => new { g.Id, g.RentalPricePerHour }) + .ToListAsync(); + + var carPrices = cars.Join(generations, + c => c.ModelGenerationId, + g => g.Id, + (c, g) => new { CarId = c.Id, Price = g.RentalPricePerHour }) + .ToDictionary(x => x.CarId, x => x.Price); + + var topClientStats = rentals + .GroupBy(r => r.ClientId) + .Select(g => new + { + ClientId = g.Key, + TotalAmount = g.Sum(r => r.RentalHours * carPrices.GetValueOrDefault(r.CarId, 0)) + }) + .OrderByDescending(x => x.TotalAmount) + .Take(5) + .ToList(); + + var topClientIds = topClientStats.Select(x => x.ClientId).ToList(); + + var clients = await clientsRepo.GetQueryable() + .Where(c => topClientIds.Contains(c.Id)) + .ToListAsync(); + + var clientsDict = clients.ToDictionary(c => c.Id); + + var result = topClientStats + .Where(x => clientsDict.ContainsKey(x.ClientId)) + .Select(x => new ClientRentalAmountDto( + mapper.Map(clientsDict[x.ClientId]), + x.TotalAmount)) + .ToList(); + + return Ok(result); + } +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.API/Controllers/CarModelsController.cs b/CarRental/CarRental/CarRental.API/Controllers/CarModelsController.cs new file mode 100644 index 000000000..faeb8d19d --- /dev/null +++ b/CarRental/CarRental/CarRental.API/Controllers/CarModelsController.cs @@ -0,0 +1,92 @@ +using AutoMapper; +using CarRental.Application.Contracts.Dto; +using CarRental.Domain.Entities; +using CarRental.Domain.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace CarRental.Api.Controllers; + +/// +/// Контроллер для управления моделями автомобилей +/// +[ApiController] +[Route("api/car-models")] +public class CarModelsController( + IRepository repo, + IMapper mapper) : ControllerBase +{ + /// + /// Получает доступ ко всем моделям автомобилей + /// + /// Список всех моделей автомобилей + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetAll() + { + var entities = await repo.GetAllAsync(); + var dtos = mapper.Map>(entities); + return Ok(dtos); + } + + /// + /// Получает модель автомобиля по идентификатору + /// + /// Идентификатор модели + /// Модель автомобиля + [HttpGet("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> Get(int id) + { + var entity = await repo.GetByIdAsync(id); + if (entity == null) return NotFound(); + var dto = mapper.Map(entity); + return Ok(dto); + } + + /// + /// Создает новую модель автомобиля + /// + /// Данные для создания модели + /// Созданная модель автомобиля + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task> Create([FromBody] CarModelEditDto dto) + { + var entity = mapper.Map(dto); + var created = await repo.AddAsync(entity); + var resultDto = mapper.Map(created); + return CreatedAtAction(nameof(Get), new { id = resultDto.Id }, resultDto); + } + + /// + /// Обновляет существующую модель автомобиля + /// + /// Идентификатор модели + /// Обновленные данные модели + /// Обновленная модель автомобиля + [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> Update(int id, [FromBody] CarModelEditDto dto) + { + var entity = await repo.GetByIdAsync(id); + if (entity == null) return NotFound(); + mapper.Map(dto, entity); + await repo.UpdateAsync(entity); + var resultDto = mapper.Map(entity); + return Ok(resultDto); + } + + /// + /// Удалить модель автомобиля + /// + /// Идентификатор модели + [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task Delete(int id) + { + await repo.DeleteAsync(id); + return NoContent(); + } +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.API/Controllers/CarsController.cs b/CarRental/CarRental/CarRental.API/Controllers/CarsController.cs new file mode 100644 index 000000000..7f99fbd4a --- /dev/null +++ b/CarRental/CarRental/CarRental.API/Controllers/CarsController.cs @@ -0,0 +1,124 @@ +using AutoMapper; +using CarRental.Application.Contracts.Dto; +using CarRental.Domain.Entities; +using CarRental.Domain.Interfaces; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace CarRental.Api.Controllers; + +/// +/// +/// +[ApiController] +[Route("api/cars")] +public class CarsController( + IRepository repo, + IRepository modelGenerationRepo, + IMapper mapper) : ControllerBase +{ + /// + /// + /// + /// + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetAll() + { + var entities = await repo.GetAllAsync( + include: query => query + .Include(c => c.ModelGeneration) + .ThenInclude(mg => mg.Model)); + var dtos = mapper.Map>(entities); + return Ok(dtos); + } + + /// + /// ID + /// + /// + /// + [HttpGet("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> Get(int id) + { + var entity = await repo.GetByIdAsync(id, + include: query => query + .Include(c => c.ModelGeneration) + .ThenInclude(mg => mg.Model)); + if (entity == null) return NotFound(); + var dto = mapper.Map(entity); + return Ok(dto); + } + + /// + /// + /// + /// + /// + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task> Create([FromBody] CarEditDto dto) + { + var modelGeneration = await modelGenerationRepo.GetByIdAsync(dto.ModelGenerationId); + if (modelGeneration == null) + return BadRequest($"Model generation with Id {dto.ModelGenerationId} does not exist."); + + var entity = mapper.Map(dto); + var created = await repo.AddAsync(entity); + + // DTO + var carWithIncludes = await repo.GetByIdAsync(created.Id, + include: query => query + .Include(c => c.ModelGeneration) + .ThenInclude(mg => mg.Model)); + var resultDto = mapper.Map(carWithIncludes); + + return CreatedAtAction(nameof(Get), new { id = resultDto.Id }, resultDto); + } + + /// + /// + /// + /// + /// + /// + [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task> Update(int id, [FromBody] CarEditDto dto) + { + var entity = await repo.GetByIdAsync(id); + if (entity == null) return NotFound(); + + var modelGeneration = await modelGenerationRepo.GetByIdAsync(dto.ModelGenerationId); + if (modelGeneration == null) + return BadRequest($"Model generation with Id {dto.ModelGenerationId} does not exist."); + + mapper.Map(dto, entity); + await repo.UpdateAsync(entity); + + var updatedWithIncludes = await repo.GetByIdAsync(entity.Id, + include: query => query + .Include(c => c.ModelGeneration) + .ThenInclude(mg => mg.Model)); + var resultDto = mapper.Map(updatedWithIncludes); + + return Ok(resultDto); + } + + /// + /// + /// + /// + [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task Delete(int id) + { + await repo.DeleteAsync(id); + return NoContent(); + } +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.API/Controllers/ClientsController.cs b/CarRental/CarRental/CarRental.API/Controllers/ClientsController.cs new file mode 100644 index 000000000..40d2cf804 --- /dev/null +++ b/CarRental/CarRental/CarRental.API/Controllers/ClientsController.cs @@ -0,0 +1,92 @@ +using AutoMapper; +using CarRental.Application.Contracts.Dto; +using CarRental.Domain.Entities; +using CarRental.Domain.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace CarRental.Api.Controllers; + +/// +/// Контроллер для управления клиентами +/// +[ApiController] +[Route("api/clients")] +public class ClientsController( + IRepository repo, + IMapper mapper) : ControllerBase +{ + /// + /// Получает всех клиентов + /// + /// Список всех клиентов + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetAll() + { + var entities = await repo.GetAllAsync(); + var dtos = mapper.Map>(entities); + return Ok(dtos); + } + + /// + /// Получает клиента по ID + /// + /// Идентификатор клиента + /// Клиент + [HttpGet("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> Get(int id) + { + var entity = await repo.GetByIdAsync(id); + if (entity == null) return NotFound(); + var dto = mapper.Map(entity); + return Ok(dto); + } + + /// + /// Создает нового клиента + /// + /// Данные для создания клиента + /// Созданный клиент + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + public async Task> Create([FromBody] ClientEditDto dto) + { + var entity = mapper.Map(dto); + var created = await repo.AddAsync(entity); + var resultDto = mapper.Map(created); + return CreatedAtAction(nameof(Get), new { id = resultDto.Id }, resultDto); + } + + /// + /// Обновляет существующий клиент + /// + /// Идентификатор клиента + /// Обновленные данные о клиентах + /// Обновленный клиент + [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> Update(int id, [FromBody] ClientEditDto dto) + { + var entity = await repo.GetByIdAsync(id); + if (entity == null) return NotFound(); + mapper.Map(dto, entity); + await repo.UpdateAsync(entity); + var resultDto = mapper.Map(entity); + return Ok(resultDto); + } + + /// + /// Удаляет клиента + /// + /// Идентификатор клиента + [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task Delete(int id) + { + await repo.DeleteAsync(id); + return NoContent(); + } +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.API/Controllers/ModelGenerationsController.cs b/CarRental/CarRental/CarRental.API/Controllers/ModelGenerationsController.cs new file mode 100644 index 000000000..1a4d77932 --- /dev/null +++ b/CarRental/CarRental/CarRental.API/Controllers/ModelGenerationsController.cs @@ -0,0 +1,117 @@ +using AutoMapper; +using CarRental.Application.Contracts.Dto; +using CarRental.Domain.Entities; +using CarRental.Domain.Interfaces; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace CarRental.Api.Controllers; + +/// +/// +/// +[ApiController] +[Route("api/model-generations")] +public class ModelGenerationsController( + IRepository repo, + IRepository carModelRepo, + IMapper mapper) : ControllerBase +{ + /// + /// + /// + /// + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetAll() + { + var entities = await repo.GetAllAsync( + include: query => query.Include(mg => mg.Model)); + var dtos = mapper.Map>(entities); + return Ok(dtos); + } + + /// + /// ID + /// + /// + /// + [HttpGet("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> Get(int id) + { + var entity = await repo.GetByIdAsync(id, + include: query => query.Include(mg => mg.Model)); + if (entity == null) return NotFound(); + var dto = mapper.Map(entity); + return Ok(dto); + } + + /// + /// + /// + /// + /// + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task> Create([FromBody] ModelGenerationEditDto dto) + { + var carModel = await carModelRepo.GetByIdAsync(dto.ModelId); + if (carModel == null) + return BadRequest($"Car model with Id {dto.ModelId} does not exist."); + + var entity = mapper.Map(dto); + var created = await repo.AddAsync(entity); + + // DTO + var generationWithIncludes = await repo.GetByIdAsync(created.Id, + include: query => query.Include(mg => mg.Model)); + var resultDto = mapper.Map(generationWithIncludes); + + return CreatedAtAction(nameof(Get), new { id = resultDto.Id }, resultDto); + } + + /// + /// + /// + /// + /// + /// + [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task> Update(int id, [FromBody] ModelGenerationEditDto dto) + { + var entity = await repo.GetByIdAsync(id); + if (entity == null) return NotFound(); + + var carModel = await carModelRepo.GetByIdAsync(dto.ModelId); + if (carModel == null) + return BadRequest($"Car model with Id {dto.ModelId} does not exist."); + + mapper.Map(dto, entity); + await repo.UpdateAsync(entity); + + // DTO + var updatedWithIncludes = await repo.GetByIdAsync(entity.Id, + include: query => query.Include(mg => mg.Model)); + var resultDto = mapper.Map(updatedWithIncludes); + + return Ok(resultDto); + } + + /// + /// + /// + /// + [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task Delete(int id) + { + await repo.DeleteAsync(id); + return NoContent(); + } +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.API/Controllers/RentalsController.cs b/CarRental/CarRental/CarRental.API/Controllers/RentalsController.cs new file mode 100644 index 000000000..f02632935 --- /dev/null +++ b/CarRental/CarRental/CarRental.API/Controllers/RentalsController.cs @@ -0,0 +1,140 @@ +using AutoMapper; +using CarRental.Application.Contracts.Dto; +using CarRental.Domain.Entities; +using CarRental.Domain.Interfaces; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace CarRental.Api.Controllers; + +/// +/// Контроллер для управления арендой +/// +[ApiController] +[Route("api/rentals")] +public class RentalsController( + IRepository repo, + IRepository carRepo, + IRepository clientRepo, + IMapper mapper) : ControllerBase +{ + /// + /// Получает все аренды + /// + /// Список всех объектов аренды + [HttpGet] + [ProducesResponseType(StatusCodes.Status200OK)] + public async Task>> GetAll() + { + var entities = await repo.GetAllAsync( + include: query => query + .Include(r => r.Car) + .ThenInclude(c => c.ModelGeneration) + .ThenInclude(mg => mg.Model) + .Include(r => r.Client)); + var dtos = mapper.Map>(entities); + return Ok(dtos); + } + + /// + /// Получает аренду по ID + /// + /// Идентификатор аренды + /// Аренда + [HttpGet("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task> Get(int id) + { + var entity = await repo.GetByIdAsync(id, + include: query => query + .Include(r => r.Car) + .ThenInclude(c => c.ModelGeneration) + .ThenInclude(mg => mg.Model) + .Include(r => r.Client)); + if (entity == null) return NotFound(); + var dto = mapper.Map(entity); + return Ok(dto); + } + + /// + /// Создает новую аренду + /// + /// Данные о создании аренды + /// Созданная аренда + [HttpPost] + [ProducesResponseType(StatusCodes.Status201Created)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task> Create([FromBody] RentalEditDto dto) + { + var car = await carRepo.GetByIdAsync(dto.CarId); + if (car == null) + return BadRequest($"Car with Id {dto.CarId} does not exist."); + + var client = await clientRepo.GetByIdAsync(dto.ClientId); + if (client == null) + return BadRequest($"Client with Id {dto.ClientId} does not exist."); + + var entity = mapper.Map(dto); + var created = await repo.AddAsync(entity); + + var rentalWithIncludes = await repo.GetByIdAsync(created.Id, + include: query => query + .Include(r => r.Car) + .ThenInclude(c => c.ModelGeneration) + .ThenInclude(mg => mg.Model) + .Include(r => r.Client)); + var resultDto = mapper.Map(rentalWithIncludes); + + return CreatedAtAction(nameof(Get), new { id = resultDto.Id }, resultDto); + } + + /// + /// Обновляет существующую аренду + /// + /// Идентификатор аренды + /// Обновленные данные об аренде + /// Обновленная аренда + [HttpPut("{id}")] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public async Task> Update(int id, [FromBody] RentalEditDto dto) + { + var entity = await repo.GetByIdAsync(id); + if (entity == null) return NotFound(); + + var car = await carRepo.GetByIdAsync(dto.CarId); + if (car == null) + return BadRequest($"Car with Id {dto.CarId} does not exist."); + + var client = await clientRepo.GetByIdAsync(dto.ClientId); + if (client == null) + return BadRequest($"Client with Id {dto.ClientId} does not exist."); + + mapper.Map(dto, entity); + await repo.UpdateAsync(entity); + + var updatedWithIncludes = await repo.GetByIdAsync(entity.Id, + include: query => query + .Include(r => r.Car) + .ThenInclude(c => c.ModelGeneration) + .ThenInclude(mg => mg.Model) + .Include(r => r.Client)); + var resultDto = mapper.Map(updatedWithIncludes); + + return Ok(resultDto); + } + + /// + /// Удаляет аренду + /// + /// Идентификатор аренды + [HttpDelete("{id}")] + [ProducesResponseType(StatusCodes.Status204NoContent)] + public async Task Delete(int id) + { + await repo.DeleteAsync(id); + return NoContent(); + } +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.API/Program.cs b/CarRental/CarRental/CarRental.API/Program.cs new file mode 100644 index 000000000..6f20b751c --- /dev/null +++ b/CarRental/CarRental/CarRental.API/Program.cs @@ -0,0 +1,71 @@ +using CarRental.Application.Contracts; +using CarRental.Domain.Data; +using CarRental.Domain.Entities; +using CarRental.Domain.Interfaces; +using CarRental.Infrastructure.Persistence; +using CarRental.Infrastructure.Repositories; +using Microsoft.EntityFrameworkCore; +using System.Text.Json.Serialization; + +var builder = WebApplication.CreateBuilder(args); + +builder.AddServiceDefaults(); + +builder.Services.AddSingleton(); + +builder.Services.AddControllers().AddJsonOptions(options => +{ + options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); +}); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + var basePath = AppContext.BaseDirectory; + + var xmlApiPath = Path.Combine(basePath, "CarRental.Api.xml"); + if (File.Exists(xmlApiPath)) + { + c.IncludeXmlComments(xmlApiPath, includeControllerXmlComments: true); + } + + var xmlContractsPath = Path.Combine(basePath, "CarRental.Application.Contracts.xml"); + if (File.Exists(xmlContractsPath)) + { + c.IncludeXmlComments(xmlContractsPath); + } + + var xmlDomainPath = Path.Combine(basePath, "CarRental.Domain.xml"); + if (File.Exists(xmlDomainPath)) + { + c.IncludeXmlComments(xmlDomainPath); + } +}); + +builder.Services.AddAutoMapper(cfg => cfg.AddProfile()); + +builder.Services.AddDbContext(options => + options.UseSqlServer( + builder.Configuration.GetConnectionString("DefaultConnection"))); + +builder.Services.AddScoped, DbRepository>(); +builder.Services.AddScoped, DbRepository>(); +builder.Services.AddScoped, DbRepository>(); +builder.Services.AddScoped, DbRepository>(); +builder.Services.AddScoped, DbRepository>(); + +var app = builder.Build(); + +using (var scope = app.Services.CreateScope()) +{ + var db = scope.ServiceProvider.GetRequiredService(); + db.Database.Migrate(); +} + +app.UseSwagger(); +app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "Car Rental API")); +app.UseHttpsRedirection(); +app.UseAuthorization(); +app.MapDefaultEndpoints(); +app.MapControllers(); +app.Run(); \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.API/Properties/launchSettings.json b/CarRental/CarRental/CarRental.API/Properties/launchSettings.json new file mode 100644 index 000000000..8abf301bf --- /dev/null +++ b/CarRental/CarRental/CarRental.API/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:22448", + "sslPort": 44345 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5208", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7197;http://localhost:5208", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/CarRental/CarRental/CarRental.API/appsettings.Development.json b/CarRental/CarRental/CarRental.API/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/CarRental/CarRental/CarRental.API/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/CarRental/CarRental/CarRental.API/appsettings.json b/CarRental/CarRental/CarRental.API/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/CarRental/CarRental/CarRental.API/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/CarRental/CarRental/CarRental.AppHost/CarRental.AppHost.csproj b/CarRental/CarRental/CarRental.AppHost/CarRental.AppHost.csproj new file mode 100644 index 000000000..3b3d5007d --- /dev/null +++ b/CarRental/CarRental/CarRental.AppHost/CarRental.AppHost.csproj @@ -0,0 +1,21 @@ + + + + + Exe + net8.0 + enable + enable + true + 191c3bb9-6290-42d5-81ef-3f797aee0fdb + + + + + + + + + + + \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.AppHost/Program.cs b/CarRental/CarRental/CarRental.AppHost/Program.cs new file mode 100644 index 000000000..f0a289646 --- /dev/null +++ b/CarRental/CarRental/CarRental.AppHost/Program.cs @@ -0,0 +1,11 @@ +var builder = DistributedApplication.CreateBuilder(args); + + +var sqlServer = builder.AddSqlServer("carrental-sql-server") + .AddDatabase("CarRentalDb"); + +var api = builder.AddProject("carrental-api") + .WithReference(sqlServer, "DefaultConnection") + .WaitFor(sqlServer); + +builder.Build().Run(); \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.AppHost/Properties/launchSettings.json b/CarRental/CarRental/CarRental.AppHost/Properties/launchSettings.json new file mode 100644 index 000000000..a37731897 --- /dev/null +++ b/CarRental/CarRental/CarRental.AppHost/Properties/launchSettings.json @@ -0,0 +1,17 @@ +{ + "profiles": { + "CarRental.AppHost": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:15058", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19141", + "DOTNET_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20144", + "ASPIRE_ALLOW_UNSECURED_TRANSPORT": "true" + } + } + } +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.AppHost/appsettings.Development.json b/CarRental/CarRental/CarRental.AppHost/appsettings.Development.json new file mode 100644 index 000000000..b09d7ac12 --- /dev/null +++ b/CarRental/CarRental/CarRental.AppHost/appsettings.Development.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "None" + } + }, + "Aspire": { + "Dashboard": { + "Frontend": { + "BrowserAuthMode": "Unsecured" + }, + "ResourceServiceClient": { + "AuthMode": "Unsecured" + } + } + } +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.AppHost/appsettings.json b/CarRental/CarRental/CarRental.AppHost/appsettings.json new file mode 100644 index 000000000..c093f65d5 --- /dev/null +++ b/CarRental/CarRental/CarRental.AppHost/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "DefaultConnection": "" + } +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj b/CarRental/CarRental/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj new file mode 100644 index 000000000..12163a6ec --- /dev/null +++ b/CarRental/CarRental/CarRental.Application.Contracts/CarRental.Application.Contracts.csproj @@ -0,0 +1,15 @@ + + + net8.0 + enable + enable + true + $(NoWarn);1591 + + + + + + + + diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/AnalyticsDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/AnalyticsDto.cs new file mode 100644 index 000000000..6be43e18d --- /dev/null +++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/AnalyticsDto.cs @@ -0,0 +1,8 @@ +namespace CarRental.Application.Contracts.Dto; + +/// +/// DTO для отображения количества арендованных автомобилей +/// +/// Информация об автомобиле +/// Количество прокатов этого автомобиля +public record CarRentalCountDto(CarGetDto Car, int RentalCount); \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarEditDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarEditDto.cs new file mode 100644 index 000000000..693139711 --- /dev/null +++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarEditDto.cs @@ -0,0 +1,13 @@ +namespace CarRental.Application.Contracts.Dto; + +/// +/// DTO для создания и обновления автомобилей +/// +/// Номерной знак автомобиля +/// Цвет автомобиля +/// Идентификатор поколения модели +public record CarEditDto( + string LicensePlate, + string Color, + int ModelGenerationId +); \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarGetDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarGetDto.cs new file mode 100644 index 000000000..61745bfbe --- /dev/null +++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarGetDto.cs @@ -0,0 +1,15 @@ +namespace CarRental.Application.Contracts.Dto; + +/// +/// DTO для получения информации об автомобиле +/// +/// Уникальный идентификатор автомобиля +/// Номерной знак автомобиля +/// Цвет автомобиля +/// Информация о создании модели, включая подробные сведения о модели +public record CarGetDto( + int Id, + string LicensePlate, + string Color, + ModelGenerationGetDto ModelGeneration +); \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarModelEditDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarModelEditDto.cs new file mode 100644 index 000000000..182a33ff5 --- /dev/null +++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarModelEditDto.cs @@ -0,0 +1,17 @@ +namespace CarRental.Application.Contracts.Dto; + +/// +/// DTO для создания и обновления моделей автомобилей +/// +/// Название модели автомобиля (например, "BMW 3 Series") +/// Тип привода (FWD, RWD, AWD, 4WD) +/// Количество посадочных мест в автомобиле +/// Тип кузова (Sedan, SUV, Coupe, и т.д.) +/// Класс автомобиля (Economy, Premium, Luxury, и т.д.) +public record CarModelEditDto( + string Name, + string DriveType, + int SeatsCount, + string BodyType, + string Class +); \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarModelGetDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarModelGetDto.cs new file mode 100644 index 000000000..bba44edb7 --- /dev/null +++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/CarModelGetDto.cs @@ -0,0 +1,19 @@ +namespace CarRental.Application.Contracts.Dto; + +/// +/// DTO для получения информации о модели автомобиля +/// +/// Уникальный идентификатор модели автомобиля +/// Название модели автомобиля (например, "BMW 3 Series") +/// Тип привода (FWD, RWD, AWD, 4WD) +/// Количество посадочных мест в автомобиле +/// Тип кузова (Sedan, SUV, Coupe, и т.д.) +/// Класс автомобиля (Economy, Premium, Luxury, и т.д.) +public record CarModelGetDto( + int Id, + string Name, + string DriveType, + int SeatsCount, + string BodyType, + string Class +); \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientAnalyticsDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientAnalyticsDto.cs new file mode 100644 index 000000000..4ae25b474 --- /dev/null +++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientAnalyticsDto.cs @@ -0,0 +1,8 @@ +namespace CarRental.Application.Contracts.Dto; + +/// +/// DTO для отображения сумм арендной платы клиентов +/// +/// Информация о клиенте +/// Общая сумма арендной платы для данного клиента +public record ClientRentalAmountDto(ClientGetDto Client, decimal TotalAmount); \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientEditDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientEditDto.cs new file mode 100644 index 000000000..769d9deb4 --- /dev/null +++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientEditDto.cs @@ -0,0 +1,13 @@ +namespace CarRental.Application.Contracts.Dto; + +/// +/// DTO для создания и обновления клиентов +/// +/// Номер водительского удостоверения +/// Полное имя клиента +/// Дата рождения клиента +public record ClientEditDto( + string LicenseNumber, + string FullName, + DateOnly BirthDate +); \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientGetDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientGetDto.cs new file mode 100644 index 000000000..3f8e1c9a2 --- /dev/null +++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ClientGetDto.cs @@ -0,0 +1,15 @@ +namespace CarRental.Application.Contracts.Dto; + +/// +/// DTO для получения информации о клиенте +/// +/// Уникальный идентификатор клиента +/// Номер водительского удостоверения +/// Полное имя клиента +/// Дата рождения клиента +public record ClientGetDto( + int Id, + string LicenseNumber, + string FullName, + DateOnly BirthDate +); \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/ModelGenerationEditDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ModelGenerationEditDto.cs new file mode 100644 index 000000000..67260c51f --- /dev/null +++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ModelGenerationEditDto.cs @@ -0,0 +1,17 @@ +namespace CarRental.Application.Contracts.Dto; + +/// +/// DTO для создания и обновления поколений моделей +/// +/// Год выпуска поколения модели +/// Объем двигателя в литрах +/// Тип трансмиссии (MT, AT, CVT) +/// Стоимость аренды в час +/// Идентификатор модели автомобиля +public record ModelGenerationEditDto( + int Year, + double EngineVolume, + string Transmission, + decimal RentalPricePerHour, + int ModelId +); \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/ModelGenerationGetDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ModelGenerationGetDto.cs new file mode 100644 index 000000000..db938281b --- /dev/null +++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/ModelGenerationGetDto.cs @@ -0,0 +1,19 @@ +namespace CarRental.Application.Contracts.Dto; + +/// +/// DTO для получения информации о генерации модели +/// +/// Уникальный идентификатор генерации модели +/// Год выпуска поколения модели +/// Объем двигателя в литрах +/// Тип трансмиссии (MT, AT, вариатор) +/// Стоимость аренды в час +/// Информация о модели автомобиля +public record ModelGenerationGetDto( + int Id, + int Year, + double EngineVolume, + string Transmission, + decimal RentalPricePerHour, + CarModelGetDto Model +); \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/RentalEditDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/RentalEditDto.cs new file mode 100644 index 000000000..f8a168404 --- /dev/null +++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/RentalEditDto.cs @@ -0,0 +1,15 @@ +namespace CarRental.Application.Contracts.Dto; + +/// +/// DTO для создания и обновления проката +/// +/// Дата и время начала аренды +/// Продолжительность аренды в часах +/// Идентификатор арендованного автомобиля +/// Идентификатор клиента +public record RentalEditDto( + DateTime RentalDate, + int RentalHours, + int CarId, + int ClientId +); \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Application.Contracts/Dto/RentalGetDto.cs b/CarRental/CarRental/CarRental.Application.Contracts/Dto/RentalGetDto.cs new file mode 100644 index 000000000..eb379969b --- /dev/null +++ b/CarRental/CarRental/CarRental.Application.Contracts/Dto/RentalGetDto.cs @@ -0,0 +1,17 @@ +namespace CarRental.Application.Contracts.Dto; + +/// +/// DTO для получения информации об аренде +/// +/// Уникальный идентификатор объекта аренды +/// Дата и время начала аренды +/// Продолжительность аренды в часах +/// Информация об арендованном автомобиле +/// Информация о клиенте +public record RentalGetDto( + int Id, + DateTime RentalDate, + int RentalHours, + CarGetDto Car, + ClientGetDto Client +); \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Application.Contracts/MappingProfile.cs b/CarRental/CarRental/CarRental.Application.Contracts/MappingProfile.cs new file mode 100644 index 000000000..e396443e3 --- /dev/null +++ b/CarRental/CarRental/CarRental.Application.Contracts/MappingProfile.cs @@ -0,0 +1,37 @@ +using AutoMapper; +using CarRental.Application.Contracts.Dto; +using CarRental.Domain.Entities; +using System.Runtime.ConstrainedExecution; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace CarRental.Application.Contracts; + +public class MappingProfile : Profile +{ + public MappingProfile() + { + CreateMap() + .ForMember(dest => dest.ModelGeneration, + opt => opt.MapFrom(src => src.ModelGeneration)); + + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap(); + CreateMap(); + + CreateMap() + .ForMember(dest => dest.Model, + opt => opt.MapFrom(src => src.Model)); + CreateMap(); + + CreateMap() + .ForMember(dest => dest.Car, + opt => opt.MapFrom(src => src.Car)) + .ForMember(dest => dest.Client, + opt => opt.MapFrom(src => src.Client)); + CreateMap(); + } +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Domain/CarRental.Domain.csproj b/CarRental/CarRental/CarRental.Domain/CarRental.Domain.csproj new file mode 100644 index 000000000..4b9cc15f8 --- /dev/null +++ b/CarRental/CarRental/CarRental.Domain/CarRental.Domain.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + CarRental.Domain + enable + enable + + + + + + + diff --git a/CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs b/CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs new file mode 100644 index 000000000..49117dea4 --- /dev/null +++ b/CarRental/CarRental/CarRental.Domain/Data/CarRentalFixture.cs @@ -0,0 +1,119 @@ +using CarRental.Domain.Entities; + +namespace CarRental.Domain.Data; + +/// +/// Fixture с тестовыми данными +/// +public class CarRentalFixture +{ + public List CarModels { get; } + public List ModelGenerations { get; } + public List Cars { get; } + public List Clients { get; } + public List Rentals { get; } + + public CarRentalFixture() + { + CarModels = + [ + new() { Id = 1, Name = "BMW 3 Series", DriveType = "RWD", SeatsCount = 5, BodyType = "Sedan", Class = "Premium" }, + new() { Id = 2, Name = "Ford Mustang", DriveType = "RWD", SeatsCount = 4, BodyType = "Coupe", Class = "Sports" }, + new() { Id = 3, Name = "Honda Civic", DriveType = "FWD", SeatsCount = 5, BodyType = "Sedan", Class = "Compact" }, + new() { Id = 4, Name = "Jeep Wrangler", DriveType = "4WD", SeatsCount = 5, BodyType = "SUV", Class = "Off-road" }, + new() { Id = 5, Name = "Porsche 911", DriveType = "RWD", SeatsCount = 4, BodyType = "Coupe", Class = "Luxury" }, + new() { Id = 6, Name = "Chevrolet Tahoe", DriveType = "AWD", SeatsCount = 8, BodyType = "SUV", Class = "Full-size" }, + new() { Id = 7, Name = "Lada Vesta", DriveType = "FWD", SeatsCount = 5, BodyType = "Sedan", Class = "Economy" }, + new() { Id = 8, Name = "Subaru Outback", DriveType = "AWD", SeatsCount = 5, BodyType = "SUV", Class = "Mid-size" }, + new() { Id = 9, Name = "GAZ Gazelle Next", DriveType = "RWD", SeatsCount = 3, BodyType = "Van", Class = "Commercial" }, + new() { Id = 10, Name = "Toyota Prius", DriveType = "FWD", SeatsCount = 5, BodyType = "Hatchback", Class = "Hybrid" }, + new() { Id = 11, Name = "UAZ Patriot", DriveType = "4WD", SeatsCount = 5, BodyType = "SUV", Class = "Off-road" }, + new() { Id = 12, Name = "Lexus RX", DriveType = "AWD", SeatsCount = 5, BodyType = "SUV", Class = "Premium" }, + new() { Id = 13, Name = "Range Rover Sport", DriveType = "AWD", SeatsCount = 5, BodyType = "SUV", Class = "Luxury" }, + new() { Id = 14, Name = "Audi A4", DriveType = "AWD", SeatsCount = 5, BodyType = "Sedan", Class = "Premium" }, + new() { Id = 15, Name = "Lada Niva Travel", DriveType = "4WD", SeatsCount = 5, BodyType = "SUV", Class = "Off-road" } + ]; + + + ModelGenerations = + [ + new() { Id = 1, Year = 2023, EngineVolume = 2.0, Transmission = "AT", RentalPricePerHour = 2200, ModelId = 1 }, + new() { Id = 2, Year = 2022, EngineVolume = 5.0, Transmission = "AT", RentalPricePerHour = 5000, ModelId = 2 }, + new() { Id = 3, Year = 2024, EngineVolume = 1.5, Transmission = "CVT", RentalPricePerHour = 1200, ModelId = 3 }, + new() { Id = 4, Year = 2023, EngineVolume = 3.6, Transmission = "AT", RentalPricePerHour = 2800, ModelId = 4 }, + new() { Id = 5, Year = 2024, EngineVolume = 3.0, Transmission = "AT", RentalPricePerHour = 8000, ModelId = 5 }, + new() { Id = 6, Year = 2022, EngineVolume = 5.3, Transmission = "AT", RentalPricePerHour = 3500, ModelId = 6 }, + new() { Id = 7, Year = 2023, EngineVolume = 1.6, Transmission = "MT", RentalPricePerHour = 700, ModelId = 7 }, + new() { Id = 8, Year = 2024, EngineVolume = 2.5, Transmission = "AT", RentalPricePerHour = 1800, ModelId = 8 }, + new() { Id = 9, Year = 2022, EngineVolume = 2.7, Transmission = "MT", RentalPricePerHour = 1500, ModelId = 9 }, + new() { Id = 10, Year = 2023, EngineVolume = 1.8, Transmission = "CVT", RentalPricePerHour = 1600, ModelId = 10 }, + new() { Id = 11, Year = 2022, EngineVolume = 2.7, Transmission = "MT", RentalPricePerHour = 1400, ModelId = 11 }, + new() { Id = 12, Year = 2024, EngineVolume = 3.5, Transmission = "AT", RentalPricePerHour = 3200, ModelId = 12 }, + new() { Id = 13, Year = 2023, EngineVolume = 3.0, Transmission = "AT", RentalPricePerHour = 6000, ModelId = 13 }, + new() { Id = 14, Year = 2024, EngineVolume = 2.0, Transmission = "AT", RentalPricePerHour = 2800, ModelId = 14 }, + new() { Id = 15, Year = 2023, EngineVolume = 1.7, Transmission = "MT", RentalPricePerHour = 900, ModelId = 15 } + ]; + + Cars = + [ + new() { Id = 1, LicensePlate = "A001AA163", Color = "Black", ModelGenerationId = 1 }, + new() { Id = 2, LicensePlate = "B777BC163", Color = "Red", ModelGenerationId = 2 }, + new() { Id = 3, LicensePlate = "C123ET163", Color = "White", ModelGenerationId = 3 }, + new() { Id = 4, LicensePlate = "E555KH163", Color = "Green", ModelGenerationId = 4 }, + new() { Id = 5, LicensePlate = "K234MR163", Color = "Silver", ModelGenerationId = 5 }, + new() { Id = 6, LicensePlate = "M888OA163", Color = "Gray", ModelGenerationId = 6 }, + new() { Id = 7, LicensePlate = "N456RS163", Color = "Blue", ModelGenerationId = 7 }, + new() { Id = 8, LicensePlate = "O789TU163", Color = "Brown", ModelGenerationId = 8 }, + new() { Id = 9, LicensePlate = "P321XO163", Color = "White", ModelGenerationId = 9 }, + new() { Id = 10, LicensePlate = "S654AM163", Color = "Black", ModelGenerationId = 10 }, + new() { Id = 11, LicensePlate = "T987RE163", Color = "Orange", ModelGenerationId = 11 }, + new() { Id = 12, LicensePlate = "U246KN163", Color = "White", ModelGenerationId = 12 }, + new() { Id = 13, LicensePlate = "H135VT163", Color = "Black", ModelGenerationId = 13 }, + new() { Id = 14, LicensePlate = "SH579SA163", Color = "Gray", ModelGenerationId = 14 }, + new() { Id = 15, LicensePlate = "SCH864RO163", Color = "Blue", ModelGenerationId = 15 } + ]; + + + Clients = + [ + new() { Id = 1, LicenseNumber = "2023-001", FullName = "Alexander Smirnov", BirthDate = new DateOnly(1988, 3, 15) }, + new() { Id = 2, LicenseNumber = "2022-045", FullName = "Marina Kovalenko", BirthDate = new DateOnly(1992, 7, 22) }, + new() { Id = 3, LicenseNumber = "2024-012", FullName = "Denis Popov", BirthDate = new DateOnly(1995, 11, 10) }, + new() { Id = 4, LicenseNumber = "2021-078", FullName = "Elena Vasnetsova", BirthDate = new DateOnly(1985, 5, 3) }, + new() { Id = 5, LicenseNumber = "2023-056", FullName = "Igor Kozlovsky",BirthDate = new DateOnly(1990, 9, 30) }, + new() { Id = 6, LicenseNumber = "2022-123", FullName = "Anna Orlova", BirthDate = new DateOnly(1993, 2, 14) }, + new() { Id = 7, LicenseNumber = "2024-034", FullName = "Artem Belov", BirthDate = new DateOnly(1987, 8, 18) }, + new() { Id = 8, LicenseNumber = "2021-099", FullName = "Sofia Grigorieva", BirthDate = new DateOnly(1994, 12, 25) }, + new() { Id = 9, LicenseNumber = "2023-087", FullName = "Pavel Melnikov", BirthDate = new DateOnly(1991, 6, 7) }, + new() { Id = 10, LicenseNumber = "2022-067", FullName = "Olga Zakharova", BirthDate = new DateOnly(1989, 4, 12) }, + new() { Id = 11, LicenseNumber = "2024-005", FullName = "Mikhail Tikhonov", BirthDate = new DateOnly(1996, 10, 28) }, + new() { Id = 12, LicenseNumber = "2021-112", FullName = "Ksenia Fedorova", BirthDate = new DateOnly(1986, 1, 19) }, + new() { Id = 13, LicenseNumber = "2023-092", FullName = "Roman Sokolov", BirthDate = new DateOnly(1997, 7, 3) }, + new() { Id = 14, LicenseNumber = "2022-031", FullName = "Tatiana Krylova", BirthDate = new DateOnly(1984, 3, 22) }, + new() { Id = 15, LicenseNumber = "2024-021", FullName = "Andrey Davydov", BirthDate = new DateOnly(1998, 11, 15) } + ]; + + Rentals = + [ + new() { Id = 1, CarId = 7, ClientId = 1, RentalDate = new DateTime(2024, 3, 1, 10, 0, 0), RentalHours = 48 }, + new() { Id = 2, CarId = 7, ClientId = 3, RentalDate = new DateTime(2024, 2, 25, 14, 30, 0), RentalHours = 72 }, + new() { Id = 3, CarId = 7, ClientId = 5, RentalDate = new DateTime(2024, 2, 20, 9, 15, 0), RentalHours = 24}, + new() { Id = 4, CarId = 1, ClientId = 2, RentalDate = new DateTime(2024, 2, 27, 11, 45, 0), RentalHours = 96 }, + new() { Id = 5, CarId = 1, ClientId = 4, RentalDate = new DateTime(2024, 2, 25, 16, 0, 0), RentalHours = 120}, + new() { Id = 6, CarId = 2, ClientId = 6, RentalDate = new DateTime(2024, 2, 23, 13, 20, 0), RentalHours = 72}, + new() { Id = 7, CarId = 2, ClientId = 8, RentalDate = new DateTime(2024, 2, 18, 10, 10, 0), RentalHours = 48 }, + new() { Id = 8, CarId = 3, ClientId = 7, RentalDate = new DateTime(2024, 2, 28, 8, 30, 0), RentalHours = 36 }, + new() { Id = 9, CarId = 4, ClientId = 9, RentalDate = new DateTime(2024, 2, 15, 12, 0, 0), RentalHours = 96 }, + new() { Id = 10, CarId = 5, ClientId = 10, RentalDate = new DateTime(2024, 2, 28, 7, 0, 0), RentalHours = 168 }, + new() { Id = 11, CarId = 6, ClientId = 11, RentalDate = new DateTime(2024, 2, 22, 15, 45, 0), RentalHours = 72 }, + new() { Id = 12, CarId = 8, ClientId = 12, RentalDate = new DateTime(2024, 2, 26, 9, 20, 0), RentalHours = 48 }, + new() { Id = 13, CarId = 9, ClientId = 13, RentalDate = new DateTime(2024, 2, 29, 22, 0, 0), RentalHours = 60 }, + new() { Id = 14, CarId = 10, ClientId = 14, RentalDate = new DateTime(2024, 2, 24, 11, 30, 0), RentalHours = 96 }, + new() { Id = 15, CarId = 11, ClientId = 15, RentalDate = new DateTime(2024, 2, 10, 14, 15, 0), RentalHours = 120 }, + new() { Id = 16, CarId = 12, ClientId = 1, RentalDate = new DateTime(2024, 2, 29, 14, 0, 0), RentalHours = 48 }, + new() { Id = 17, CarId = 13, ClientId = 2, RentalDate = new DateTime(2024, 2, 5, 16, 45, 0), RentalHours = 72 }, + new() { Id = 18, CarId = 14, ClientId = 3, RentalDate = new DateTime(2024, 2, 12, 10, 10, 0), RentalHours = 36 }, + new() { Id = 19, CarId = 15, ClientId = 4, RentalDate = new DateTime(2024, 2, 16, 13, 30, 0), RentalHours = 84 } + ]; + } +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Domain/Entities/Car.cs b/CarRental/CarRental/CarRental.Domain/Entities/Car.cs new file mode 100644 index 000000000..225757a16 --- /dev/null +++ b/CarRental/CarRental/CarRental.Domain/Entities/Car.cs @@ -0,0 +1,42 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace CarRental.Domain.Entities; + +/// +/// Автомобиль в парке +/// +[Table("cars")] +public class Car +{ + /// + /// Уникальный ID автомобиля в парке + /// + [Column("id")] + public int Id { get; set; } + + /// + /// Государственный номер + /// + [Column("license_plate")] + [MaxLength(20)] + public required string LicensePlate { get; set; } + + /// + /// Цвет кузова + /// + [Column("color")] + [MaxLength(30)] + public required string Color { get; set; } + + /// + /// Внешний ключ на поколение модели + /// + [Column("model_generation_id")] + public required int ModelGenerationId { get; set; } + + /// + /// Ссылка на поколение модели этого автомобиля + /// + public ModelGeneration? ModelGeneration { get; set; } +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Domain/Entities/CarModel.cs b/CarRental/CarRental/CarRental.Domain/Entities/CarModel.cs new file mode 100644 index 000000000..67883f208 --- /dev/null +++ b/CarRental/CarRental/CarRental.Domain/Entities/CarModel.cs @@ -0,0 +1,51 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace CarRental.Domain.Entities; + +/// +/// Модель автомобиля (справочник) +/// +[Table("car_models")] +public class CarModel +{ + /// + /// Уникальный идентификатор модели + /// + [Column("id")] + public int Id { get; set; } + + /// + /// Название модели (например, "Toyota Camry") + /// + [Column("name")] + [MaxLength(50)] + public required string Name { get; set; } + + /// + /// Тип привода + /// + [Column("drive_type")] + [MaxLength(10)] + public required string DriveType { get; set; } + + /// + /// Количество посадочных мест + /// + [Column("seats_count")] + public required int SeatsCount { get; set; } + + /// + /// Тип кузова + /// + [Column("body_type")] + [MaxLength(20)] + public required string BodyType { get; set; } + + /// + /// Класс автомобиля + /// + [Column("class")] + [MaxLength(20)] + public required string Class { get; set; } +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Domain/Entities/Client.cs b/CarRental/CarRental/CarRental.Domain/Entities/Client.cs new file mode 100644 index 000000000..831cf2e4b --- /dev/null +++ b/CarRental/CarRental/CarRental.Domain/Entities/Client.cs @@ -0,0 +1,37 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace CarRental.Domain.Entities; + +/// +/// Клиент пункта проката +/// +[Table("clients")] +public class Client +{ + /// + /// Уникальный ID клиента + /// + [Column("id")] + public int Id { get; set; } + + /// + /// Номер водительского удостоверения (уникален) + /// + [Column("license_number")] + [MaxLength(20)] + public required string LicenseNumber { get; set; } + + /// + /// Полное имя клиента (ФИО) + /// + [Column("full_name")] + [MaxLength(100)] + public required string FullName { get; set; } + + /// + /// Дата рождения + /// + [Column("birth_date")] + public required DateOnly BirthDate { get; set; } +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Domain/Entities/ModelGeneration.cs b/CarRental/CarRental/CarRental.Domain/Entities/ModelGeneration.cs new file mode 100644 index 000000000..0cbdd814b --- /dev/null +++ b/CarRental/CarRental/CarRental.Domain/Entities/ModelGeneration.cs @@ -0,0 +1,53 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace CarRental.Domain.Entities; + +/// +/// Поколение модели (справочник) +/// +[Table("model_generations")] +public class ModelGeneration +{ + /// + /// Уникальный идентификатор поколения + /// + [Column("id")] + public int Id { get; set; } + + /// + /// Год выпуска + /// + [Column("year")] + public required int Year { get; set; } + + /// + /// Объем двигателя в литрах + /// + [Column("engine_volume")] + public required double EngineVolume { get; set; } + + /// + /// Тип коробки передач + /// + [Column("transmission")] + [MaxLength(10)] + public required string Transmission { get; set; } + + /// + /// Стоимость аренды в час + /// + [Column("rental_price_per_hour")] + public required decimal RentalPricePerHour { get; set; } + + /// + /// Внешний ключ на модель + /// + [Column("model_id")] + public required int ModelId { get; set; } + + /// + /// Ссылка на модель (навигационное свойство) + /// + public CarModel? Model { get; set; } +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Domain/Entities/Rental.cs b/CarRental/CarRental/CarRental.Domain/Entities/Rental.cs new file mode 100644 index 000000000..2cfafc7b7 --- /dev/null +++ b/CarRental/CarRental/CarRental.Domain/Entities/Rental.cs @@ -0,0 +1,52 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace CarRental.Domain.Entities; + +/// +/// Договор аренды (контракт) +/// Фиксирует факт выдачи автомобиля клиенту +/// +[Table("rentals")] +public class Rental +{ + /// + /// Уникальный ID контракта + /// + [Column("id")] + public int Id { get; set; } + + /// + /// Дата и время выдачи автомобиля клиенту + /// + [Column("rental_date")] + public required DateTime RentalDate { get; set; } + + /// + /// Длительность аренды в часах + /// + [Column("rental_hours")] + public required int RentalHours { get; set; } + + /// + /// Внешний ключ на автомобиль + /// + [Column("car_id")] + public required int CarId { get; set; } + + /// + /// Внешний ключ на клиента + /// + [Column("client_id")] + public required int ClientId { get; set; } + + /// + /// Ссылка на арендуемый автомобиль + /// + public Car? Car { get; set; } + + /// + /// Ссылка на клиента, арендовавшего машину + /// + public Client? Client { get; set; } +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Domain/Interfaces/IRepository.cs b/CarRental/CarRental/CarRental.Domain/Interfaces/IRepository.cs new file mode 100644 index 000000000..26c2c675c --- /dev/null +++ b/CarRental/CarRental/CarRental.Domain/Interfaces/IRepository.cs @@ -0,0 +1,16 @@ +using System.Linq.Expressions; + +namespace CarRental.Domain.Interfaces; + +public interface IRepository where T : class +{ + public Task GetByIdAsync(int id); + public Task> GetAllAsync(); + public Task AddAsync(T entity); + public Task UpdateAsync(T entity); + public Task DeleteAsync(int id); + + public Task GetByIdAsync(int id, Func, IQueryable>? include = null); + public Task> GetAllAsync(Func, IQueryable>? include = null); + public IQueryable GetQueryable(Func, IQueryable>? include = null); +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Infrastructure/CarRental.Infrastructure.csproj b/CarRental/CarRental/CarRental.Infrastructure/CarRental.Infrastructure.csproj new file mode 100644 index 000000000..0513ff5aa --- /dev/null +++ b/CarRental/CarRental/CarRental.Infrastructure/CarRental.Infrastructure.csproj @@ -0,0 +1,13 @@ + + + net8.0 + enable + enable + + + + + + + + \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Infrastructure/Migrations/20260220055307_InitialCreate.Designer.cs b/CarRental/CarRental/CarRental.Infrastructure/Migrations/20260220055307_InitialCreate.Designer.cs new file mode 100644 index 000000000..52321e4f6 --- /dev/null +++ b/CarRental/CarRental/CarRental.Infrastructure/Migrations/20260220055307_InitialCreate.Designer.cs @@ -0,0 +1,889 @@ +// +using System; +using CarRental.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CarRental.Infrastructure.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260220055307_InitialCreate")] + partial class InitialCreate + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CarRental.Domain.Entities.Car", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Color") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)") + .HasColumnName("color"); + + b.Property("LicensePlate") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)") + .HasColumnName("license_plate"); + + b.Property("ModelGenerationId") + .HasColumnType("int") + .HasColumnName("model_generation_id"); + + b.HasKey("Id"); + + b.HasIndex("ModelGenerationId"); + + b.ToTable("cars"); + + b.HasData( + new + { + Id = 1, + Color = "Black", + LicensePlate = "A001AA163", + ModelGenerationId = 1 + }, + new + { + Id = 2, + Color = "Red", + LicensePlate = "B777BC163", + ModelGenerationId = 2 + }, + new + { + Id = 3, + Color = "White", + LicensePlate = "C123ET163", + ModelGenerationId = 3 + }, + new + { + Id = 4, + Color = "Green", + LicensePlate = "E555KH163", + ModelGenerationId = 4 + }, + new + { + Id = 5, + Color = "Silver", + LicensePlate = "K234MR163", + ModelGenerationId = 5 + }, + new + { + Id = 6, + Color = "Gray", + LicensePlate = "M888OA163", + ModelGenerationId = 6 + }, + new + { + Id = 7, + Color = "Blue", + LicensePlate = "N456RS163", + ModelGenerationId = 7 + }, + new + { + Id = 8, + Color = "Brown", + LicensePlate = "O789TU163", + ModelGenerationId = 8 + }, + new + { + Id = 9, + Color = "White", + LicensePlate = "P321XO163", + ModelGenerationId = 9 + }, + new + { + Id = 10, + Color = "Black", + LicensePlate = "S654AM163", + ModelGenerationId = 10 + }, + new + { + Id = 11, + Color = "Orange", + LicensePlate = "T987RE163", + ModelGenerationId = 11 + }, + new + { + Id = 12, + Color = "White", + LicensePlate = "U246KN163", + ModelGenerationId = 12 + }, + new + { + Id = 13, + Color = "Black", + LicensePlate = "H135VT163", + ModelGenerationId = 13 + }, + new + { + Id = 14, + Color = "Gray", + LicensePlate = "SH579SA163", + ModelGenerationId = 14 + }, + new + { + Id = 15, + Color = "Blue", + LicensePlate = "SCH864RO163", + ModelGenerationId = 15 + }); + }); + + modelBuilder.Entity("CarRental.Domain.Entities.CarModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BodyType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)") + .HasColumnName("body_type"); + + b.Property("Class") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)") + .HasColumnName("class"); + + b.Property("DriveType") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)") + .HasColumnName("drive_type"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("name"); + + b.Property("SeatsCount") + .HasColumnType("int") + .HasColumnName("seats_count"); + + b.HasKey("Id"); + + b.ToTable("car_models"); + + b.HasData( + new + { + Id = 1, + BodyType = "Sedan", + Class = "Premium", + DriveType = "RWD", + Name = "BMW 3 Series", + SeatsCount = 5 + }, + new + { + Id = 2, + BodyType = "Coupe", + Class = "Sports", + DriveType = "RWD", + Name = "Ford Mustang", + SeatsCount = 4 + }, + new + { + Id = 3, + BodyType = "Sedan", + Class = "Compact", + DriveType = "FWD", + Name = "Honda Civic", + SeatsCount = 5 + }, + new + { + Id = 4, + BodyType = "SUV", + Class = "Off-road", + DriveType = "4WD", + Name = "Jeep Wrangler", + SeatsCount = 5 + }, + new + { + Id = 5, + BodyType = "Coupe", + Class = "Luxury", + DriveType = "RWD", + Name = "Porsche 911", + SeatsCount = 4 + }, + new + { + Id = 6, + BodyType = "SUV", + Class = "Full-size", + DriveType = "AWD", + Name = "Chevrolet Tahoe", + SeatsCount = 8 + }, + new + { + Id = 7, + BodyType = "Sedan", + Class = "Economy", + DriveType = "FWD", + Name = "Lada Vesta", + SeatsCount = 5 + }, + new + { + Id = 8, + BodyType = "SUV", + Class = "Mid-size", + DriveType = "AWD", + Name = "Subaru Outback", + SeatsCount = 5 + }, + new + { + Id = 9, + BodyType = "Van", + Class = "Commercial", + DriveType = "RWD", + Name = "GAZ Gazelle Next", + SeatsCount = 3 + }, + new + { + Id = 10, + BodyType = "Hatchback", + Class = "Hybrid", + DriveType = "FWD", + Name = "Toyota Prius", + SeatsCount = 5 + }, + new + { + Id = 11, + BodyType = "SUV", + Class = "Off-road", + DriveType = "4WD", + Name = "UAZ Patriot", + SeatsCount = 5 + }, + new + { + Id = 12, + BodyType = "SUV", + Class = "Premium", + DriveType = "AWD", + Name = "Lexus RX", + SeatsCount = 5 + }, + new + { + Id = 13, + BodyType = "SUV", + Class = "Luxury", + DriveType = "AWD", + Name = "Range Rover Sport", + SeatsCount = 5 + }, + new + { + Id = 14, + BodyType = "Sedan", + Class = "Premium", + DriveType = "AWD", + Name = "Audi A4", + SeatsCount = 5 + }, + new + { + Id = 15, + BodyType = "SUV", + Class = "Off-road", + DriveType = "4WD", + Name = "Lada Niva Travel", + SeatsCount = 5 + }); + }); + + modelBuilder.Entity("CarRental.Domain.Entities.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("full_name"); + + b.Property("LicenseNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)") + .HasColumnName("license_number"); + + b.HasKey("Id"); + + b.ToTable("clients"); + + b.HasData( + new + { + Id = 1, + BirthDate = new DateOnly(1988, 3, 15), + FullName = "Alexander Smirnov", + LicenseNumber = "2023-001" + }, + new + { + Id = 2, + BirthDate = new DateOnly(1992, 7, 22), + FullName = "Marina Kovalenko", + LicenseNumber = "2022-045" + }, + new + { + Id = 3, + BirthDate = new DateOnly(1995, 11, 10), + FullName = "Denis Popov", + LicenseNumber = "2024-012" + }, + new + { + Id = 4, + BirthDate = new DateOnly(1985, 5, 3), + FullName = "Elena Vasnetsova", + LicenseNumber = "2021-078" + }, + new + { + Id = 5, + BirthDate = new DateOnly(1990, 9, 30), + FullName = "Igor Kozlovsky", + LicenseNumber = "2023-056" + }, + new + { + Id = 6, + BirthDate = new DateOnly(1993, 2, 14), + FullName = "Anna Orlova", + LicenseNumber = "2022-123" + }, + new + { + Id = 7, + BirthDate = new DateOnly(1987, 8, 18), + FullName = "Artem Belov", + LicenseNumber = "2024-034" + }, + new + { + Id = 8, + BirthDate = new DateOnly(1994, 12, 25), + FullName = "Sofia Grigorieva", + LicenseNumber = "2021-099" + }, + new + { + Id = 9, + BirthDate = new DateOnly(1991, 6, 7), + FullName = "Pavel Melnikov", + LicenseNumber = "2023-087" + }, + new + { + Id = 10, + BirthDate = new DateOnly(1989, 4, 12), + FullName = "Olga Zakharova", + LicenseNumber = "2022-067" + }, + new + { + Id = 11, + BirthDate = new DateOnly(1996, 10, 28), + FullName = "Mikhail Tikhonov", + LicenseNumber = "2024-005" + }, + new + { + Id = 12, + BirthDate = new DateOnly(1986, 1, 19), + FullName = "Ksenia Fedorova", + LicenseNumber = "2021-112" + }, + new + { + Id = 13, + BirthDate = new DateOnly(1997, 7, 3), + FullName = "Roman Sokolov", + LicenseNumber = "2023-092" + }, + new + { + Id = 14, + BirthDate = new DateOnly(1984, 3, 22), + FullName = "Tatiana Krylova", + LicenseNumber = "2022-031" + }, + new + { + Id = 15, + BirthDate = new DateOnly(1998, 11, 15), + FullName = "Andrey Davydov", + LicenseNumber = "2024-021" + }); + }); + + modelBuilder.Entity("CarRental.Domain.Entities.ModelGeneration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("EngineVolume") + .HasColumnType("float") + .HasColumnName("engine_volume"); + + b.Property("ModelId") + .HasColumnType("int") + .HasColumnName("model_id"); + + b.Property("RentalPricePerHour") + .HasColumnType("decimal(18,2)") + .HasColumnName("rental_price_per_hour"); + + b.Property("Transmission") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)") + .HasColumnName("transmission"); + + b.Property("Year") + .HasColumnType("int") + .HasColumnName("year"); + + b.HasKey("Id"); + + b.HasIndex("ModelId"); + + b.ToTable("model_generations"); + + b.HasData( + new + { + Id = 1, + EngineVolume = 2.0, + ModelId = 1, + RentalPricePerHour = 2200m, + Transmission = "AT", + Year = 2023 + }, + new + { + Id = 2, + EngineVolume = 5.0, + ModelId = 2, + RentalPricePerHour = 5000m, + Transmission = "AT", + Year = 2022 + }, + new + { + Id = 3, + EngineVolume = 1.5, + ModelId = 3, + RentalPricePerHour = 1200m, + Transmission = "CVT", + Year = 2024 + }, + new + { + Id = 4, + EngineVolume = 3.6000000000000001, + ModelId = 4, + RentalPricePerHour = 2800m, + Transmission = "AT", + Year = 2023 + }, + new + { + Id = 5, + EngineVolume = 3.0, + ModelId = 5, + RentalPricePerHour = 8000m, + Transmission = "AT", + Year = 2024 + }, + new + { + Id = 6, + EngineVolume = 5.2999999999999998, + ModelId = 6, + RentalPricePerHour = 3500m, + Transmission = "AT", + Year = 2022 + }, + new + { + Id = 7, + EngineVolume = 1.6000000000000001, + ModelId = 7, + RentalPricePerHour = 700m, + Transmission = "MT", + Year = 2023 + }, + new + { + Id = 8, + EngineVolume = 2.5, + ModelId = 8, + RentalPricePerHour = 1800m, + Transmission = "AT", + Year = 2024 + }, + new + { + Id = 9, + EngineVolume = 2.7000000000000002, + ModelId = 9, + RentalPricePerHour = 1500m, + Transmission = "MT", + Year = 2022 + }, + new + { + Id = 10, + EngineVolume = 1.8, + ModelId = 10, + RentalPricePerHour = 1600m, + Transmission = "CVT", + Year = 2023 + }, + new + { + Id = 11, + EngineVolume = 2.7000000000000002, + ModelId = 11, + RentalPricePerHour = 1400m, + Transmission = "MT", + Year = 2022 + }, + new + { + Id = 12, + EngineVolume = 3.5, + ModelId = 12, + RentalPricePerHour = 3200m, + Transmission = "AT", + Year = 2024 + }, + new + { + Id = 13, + EngineVolume = 3.0, + ModelId = 13, + RentalPricePerHour = 6000m, + Transmission = "AT", + Year = 2023 + }, + new + { + Id = 14, + EngineVolume = 2.0, + ModelId = 14, + RentalPricePerHour = 2800m, + Transmission = "AT", + Year = 2024 + }, + new + { + Id = 15, + EngineVolume = 1.7, + ModelId = 15, + RentalPricePerHour = 900m, + Transmission = "MT", + Year = 2023 + }); + }); + + modelBuilder.Entity("CarRental.Domain.Entities.Rental", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CarId") + .HasColumnType("int") + .HasColumnName("car_id"); + + b.Property("ClientId") + .HasColumnType("int") + .HasColumnName("client_id"); + + b.Property("RentalDate") + .HasColumnType("datetime2") + .HasColumnName("rental_date"); + + b.Property("RentalHours") + .HasColumnType("int") + .HasColumnName("rental_hours"); + + b.HasKey("Id"); + + b.HasIndex("CarId"); + + b.HasIndex("ClientId"); + + b.ToTable("rentals"); + + b.HasData( + new + { + Id = 1, + CarId = 7, + ClientId = 1, + RentalDate = new DateTime(2024, 3, 1, 10, 0, 0, 0, DateTimeKind.Unspecified), + RentalHours = 48 + }, + new + { + Id = 2, + CarId = 7, + ClientId = 3, + RentalDate = new DateTime(2024, 2, 25, 14, 30, 0, 0, DateTimeKind.Unspecified), + RentalHours = 72 + }, + new + { + Id = 3, + CarId = 7, + ClientId = 5, + RentalDate = new DateTime(2024, 2, 20, 9, 15, 0, 0, DateTimeKind.Unspecified), + RentalHours = 24 + }, + new + { + Id = 4, + CarId = 1, + ClientId = 2, + RentalDate = new DateTime(2024, 2, 27, 11, 45, 0, 0, DateTimeKind.Unspecified), + RentalHours = 96 + }, + new + { + Id = 5, + CarId = 1, + ClientId = 4, + RentalDate = new DateTime(2024, 2, 25, 16, 0, 0, 0, DateTimeKind.Unspecified), + RentalHours = 120 + }, + new + { + Id = 6, + CarId = 2, + ClientId = 6, + RentalDate = new DateTime(2024, 2, 23, 13, 20, 0, 0, DateTimeKind.Unspecified), + RentalHours = 72 + }, + new + { + Id = 7, + CarId = 2, + ClientId = 8, + RentalDate = new DateTime(2024, 2, 18, 10, 10, 0, 0, DateTimeKind.Unspecified), + RentalHours = 48 + }, + new + { + Id = 8, + CarId = 3, + ClientId = 7, + RentalDate = new DateTime(2024, 2, 28, 8, 30, 0, 0, DateTimeKind.Unspecified), + RentalHours = 36 + }, + new + { + Id = 9, + CarId = 4, + ClientId = 9, + RentalDate = new DateTime(2024, 2, 15, 12, 0, 0, 0, DateTimeKind.Unspecified), + RentalHours = 96 + }, + new + { + Id = 10, + CarId = 5, + ClientId = 10, + RentalDate = new DateTime(2024, 2, 28, 7, 0, 0, 0, DateTimeKind.Unspecified), + RentalHours = 168 + }, + new + { + Id = 11, + CarId = 6, + ClientId = 11, + RentalDate = new DateTime(2024, 2, 22, 15, 45, 0, 0, DateTimeKind.Unspecified), + RentalHours = 72 + }, + new + { + Id = 12, + CarId = 8, + ClientId = 12, + RentalDate = new DateTime(2024, 2, 26, 9, 20, 0, 0, DateTimeKind.Unspecified), + RentalHours = 48 + }, + new + { + Id = 13, + CarId = 9, + ClientId = 13, + RentalDate = new DateTime(2024, 2, 29, 22, 0, 0, 0, DateTimeKind.Unspecified), + RentalHours = 60 + }, + new + { + Id = 14, + CarId = 10, + ClientId = 14, + RentalDate = new DateTime(2024, 2, 24, 11, 30, 0, 0, DateTimeKind.Unspecified), + RentalHours = 96 + }, + new + { + Id = 15, + CarId = 11, + ClientId = 15, + RentalDate = new DateTime(2024, 2, 10, 14, 15, 0, 0, DateTimeKind.Unspecified), + RentalHours = 120 + }, + new + { + Id = 16, + CarId = 12, + ClientId = 1, + RentalDate = new DateTime(2024, 2, 29, 14, 0, 0, 0, DateTimeKind.Unspecified), + RentalHours = 48 + }, + new + { + Id = 17, + CarId = 13, + ClientId = 2, + RentalDate = new DateTime(2024, 2, 5, 16, 45, 0, 0, DateTimeKind.Unspecified), + RentalHours = 72 + }, + new + { + Id = 18, + CarId = 14, + ClientId = 3, + RentalDate = new DateTime(2024, 2, 12, 10, 10, 0, 0, DateTimeKind.Unspecified), + RentalHours = 36 + }, + new + { + Id = 19, + CarId = 15, + ClientId = 4, + RentalDate = new DateTime(2024, 2, 16, 13, 30, 0, 0, DateTimeKind.Unspecified), + RentalHours = 84 + }); + }); + + modelBuilder.Entity("CarRental.Domain.Entities.Car", b => + { + b.HasOne("CarRental.Domain.Entities.ModelGeneration", "ModelGeneration") + .WithMany() + .HasForeignKey("ModelGenerationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ModelGeneration"); + }); + + modelBuilder.Entity("CarRental.Domain.Entities.ModelGeneration", b => + { + b.HasOne("CarRental.Domain.Entities.CarModel", "Model") + .WithMany() + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Model"); + }); + + modelBuilder.Entity("CarRental.Domain.Entities.Rental", b => + { + b.HasOne("CarRental.Domain.Entities.Car", "Car") + .WithMany() + .HasForeignKey("CarId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CarRental.Domain.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Car"); + + b.Navigation("Client"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CarRental/CarRental/CarRental.Infrastructure/Migrations/20260220055307_InitialCreate.cs b/CarRental/CarRental/CarRental.Infrastructure/Migrations/20260220055307_InitialCreate.cs new file mode 100644 index 000000000..5c5f1c8d0 --- /dev/null +++ b/CarRental/CarRental/CarRental.Infrastructure/Migrations/20260220055307_InitialCreate.cs @@ -0,0 +1,274 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace CarRental.Infrastructure.Migrations +{ + /// + public partial class InitialCreate : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "car_models", + columns: table => new + { + id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + name = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + drive_type = table.Column(type: "nvarchar(10)", maxLength: 10, nullable: false), + seats_count = table.Column(type: "int", nullable: false), + body_type = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false), + @class = table.Column(name: "class", type: "nvarchar(20)", maxLength: 20, nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_car_models", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "clients", + columns: table => new + { + id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + license_number = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false), + full_name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + birth_date = table.Column(type: "date", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_clients", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "model_generations", + columns: table => new + { + id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + year = table.Column(type: "int", nullable: false), + engine_volume = table.Column(type: "float", nullable: false), + transmission = table.Column(type: "nvarchar(10)", maxLength: 10, nullable: false), + rental_price_per_hour = table.Column(type: "decimal(18,2)", nullable: false), + model_id = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_model_generations", x => x.id); + table.ForeignKey( + name: "FK_model_generations_car_models_model_id", + column: x => x.model_id, + principalTable: "car_models", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "cars", + columns: table => new + { + id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + license_plate = table.Column(type: "nvarchar(20)", maxLength: 20, nullable: false), + color = table.Column(type: "nvarchar(30)", maxLength: 30, nullable: false), + model_generation_id = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_cars", x => x.id); + table.ForeignKey( + name: "FK_cars_model_generations_model_generation_id", + column: x => x.model_generation_id, + principalTable: "model_generations", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "rentals", + columns: table => new + { + id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + rental_date = table.Column(type: "datetime2", nullable: false), + rental_hours = table.Column(type: "int", nullable: false), + car_id = table.Column(type: "int", nullable: false), + client_id = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_rentals", x => x.id); + table.ForeignKey( + name: "FK_rentals_cars_car_id", + column: x => x.car_id, + principalTable: "cars", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_rentals_clients_client_id", + column: x => x.client_id, + principalTable: "clients", + principalColumn: "id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.InsertData( + table: "car_models", + columns: new[] { "id", "body_type", "class", "drive_type", "name", "seats_count" }, + values: new object[,] + { + { 1, "Sedan", "Premium", "RWD", "BMW 3 Series", 5 }, + { 2, "Coupe", "Sports", "RWD", "Ford Mustang", 4 }, + { 3, "Sedan", "Compact", "FWD", "Honda Civic", 5 }, + { 4, "SUV", "Off-road", "4WD", "Jeep Wrangler", 5 }, + { 5, "Coupe", "Luxury", "RWD", "Porsche 911", 4 }, + { 6, "SUV", "Full-size", "AWD", "Chevrolet Tahoe", 8 }, + { 7, "Sedan", "Economy", "FWD", "Lada Vesta", 5 }, + { 8, "SUV", "Mid-size", "AWD", "Subaru Outback", 5 }, + { 9, "Van", "Commercial", "RWD", "GAZ Gazelle Next", 3 }, + { 10, "Hatchback", "Hybrid", "FWD", "Toyota Prius", 5 }, + { 11, "SUV", "Off-road", "4WD", "UAZ Patriot", 5 }, + { 12, "SUV", "Premium", "AWD", "Lexus RX", 5 }, + { 13, "SUV", "Luxury", "AWD", "Range Rover Sport", 5 }, + { 14, "Sedan", "Premium", "AWD", "Audi A4", 5 }, + { 15, "SUV", "Off-road", "4WD", "Lada Niva Travel", 5 } + }); + + migrationBuilder.InsertData( + table: "clients", + columns: new[] { "id", "birth_date", "full_name", "license_number" }, + values: new object[,] + { + { 1, new DateOnly(1988, 3, 15), "Alexander Smirnov", "2023-001" }, + { 2, new DateOnly(1992, 7, 22), "Marina Kovalenko", "2022-045" }, + { 3, new DateOnly(1995, 11, 10), "Denis Popov", "2024-012" }, + { 4, new DateOnly(1985, 5, 3), "Elena Vasnetsova", "2021-078" }, + { 5, new DateOnly(1990, 9, 30), "Igor Kozlovsky", "2023-056" }, + { 6, new DateOnly(1993, 2, 14), "Anna Orlova", "2022-123" }, + { 7, new DateOnly(1987, 8, 18), "Artem Belov", "2024-034" }, + { 8, new DateOnly(1994, 12, 25), "Sofia Grigorieva", "2021-099" }, + { 9, new DateOnly(1991, 6, 7), "Pavel Melnikov", "2023-087" }, + { 10, new DateOnly(1989, 4, 12), "Olga Zakharova", "2022-067" }, + { 11, new DateOnly(1996, 10, 28), "Mikhail Tikhonov", "2024-005" }, + { 12, new DateOnly(1986, 1, 19), "Ksenia Fedorova", "2021-112" }, + { 13, new DateOnly(1997, 7, 3), "Roman Sokolov", "2023-092" }, + { 14, new DateOnly(1984, 3, 22), "Tatiana Krylova", "2022-031" }, + { 15, new DateOnly(1998, 11, 15), "Andrey Davydov", "2024-021" } + }); + + migrationBuilder.InsertData( + table: "model_generations", + columns: new[] { "id", "engine_volume", "model_id", "rental_price_per_hour", "transmission", "year" }, + values: new object[,] + { + { 1, 2.0, 1, 2200m, "AT", 2023 }, + { 2, 5.0, 2, 5000m, "AT", 2022 }, + { 3, 1.5, 3, 1200m, "CVT", 2024 }, + { 4, 3.6000000000000001, 4, 2800m, "AT", 2023 }, + { 5, 3.0, 5, 8000m, "AT", 2024 }, + { 6, 5.2999999999999998, 6, 3500m, "AT", 2022 }, + { 7, 1.6000000000000001, 7, 700m, "MT", 2023 }, + { 8, 2.5, 8, 1800m, "AT", 2024 }, + { 9, 2.7000000000000002, 9, 1500m, "MT", 2022 }, + { 10, 1.8, 10, 1600m, "CVT", 2023 }, + { 11, 2.7000000000000002, 11, 1400m, "MT", 2022 }, + { 12, 3.5, 12, 3200m, "AT", 2024 }, + { 13, 3.0, 13, 6000m, "AT", 2023 }, + { 14, 2.0, 14, 2800m, "AT", 2024 }, + { 15, 1.7, 15, 900m, "MT", 2023 } + }); + + migrationBuilder.InsertData( + table: "cars", + columns: new[] { "id", "color", "license_plate", "model_generation_id" }, + values: new object[,] + { + { 1, "Black", "A001AA163", 1 }, + { 2, "Red", "B777BC163", 2 }, + { 3, "White", "C123ET163", 3 }, + { 4, "Green", "E555KH163", 4 }, + { 5, "Silver", "K234MR163", 5 }, + { 6, "Gray", "M888OA163", 6 }, + { 7, "Blue", "N456RS163", 7 }, + { 8, "Brown", "O789TU163", 8 }, + { 9, "White", "P321XO163", 9 }, + { 10, "Black", "S654AM163", 10 }, + { 11, "Orange", "T987RE163", 11 }, + { 12, "White", "U246KN163", 12 }, + { 13, "Black", "H135VT163", 13 }, + { 14, "Gray", "SH579SA163", 14 }, + { 15, "Blue", "SCH864RO163", 15 } + }); + + migrationBuilder.InsertData( + table: "rentals", + columns: new[] { "id", "car_id", "client_id", "rental_date", "rental_hours" }, + values: new object[,] + { + { 1, 7, 1, new DateTime(2024, 3, 1, 10, 0, 0, 0, DateTimeKind.Unspecified), 48 }, + { 2, 7, 3, new DateTime(2024, 2, 25, 14, 30, 0, 0, DateTimeKind.Unspecified), 72 }, + { 3, 7, 5, new DateTime(2024, 2, 20, 9, 15, 0, 0, DateTimeKind.Unspecified), 24 }, + { 4, 1, 2, new DateTime(2024, 2, 27, 11, 45, 0, 0, DateTimeKind.Unspecified), 96 }, + { 5, 1, 4, new DateTime(2024, 2, 25, 16, 0, 0, 0, DateTimeKind.Unspecified), 120 }, + { 6, 2, 6, new DateTime(2024, 2, 23, 13, 20, 0, 0, DateTimeKind.Unspecified), 72 }, + { 7, 2, 8, new DateTime(2024, 2, 18, 10, 10, 0, 0, DateTimeKind.Unspecified), 48 }, + { 8, 3, 7, new DateTime(2024, 2, 28, 8, 30, 0, 0, DateTimeKind.Unspecified), 36 }, + { 9, 4, 9, new DateTime(2024, 2, 15, 12, 0, 0, 0, DateTimeKind.Unspecified), 96 }, + { 10, 5, 10, new DateTime(2024, 2, 28, 7, 0, 0, 0, DateTimeKind.Unspecified), 168 }, + { 11, 6, 11, new DateTime(2024, 2, 22, 15, 45, 0, 0, DateTimeKind.Unspecified), 72 }, + { 12, 8, 12, new DateTime(2024, 2, 26, 9, 20, 0, 0, DateTimeKind.Unspecified), 48 }, + { 13, 9, 13, new DateTime(2024, 2, 29, 22, 0, 0, 0, DateTimeKind.Unspecified), 60 }, + { 14, 10, 14, new DateTime(2024, 2, 24, 11, 30, 0, 0, DateTimeKind.Unspecified), 96 }, + { 15, 11, 15, new DateTime(2024, 2, 10, 14, 15, 0, 0, DateTimeKind.Unspecified), 120 }, + { 16, 12, 1, new DateTime(2024, 2, 29, 14, 0, 0, 0, DateTimeKind.Unspecified), 48 }, + { 17, 13, 2, new DateTime(2024, 2, 5, 16, 45, 0, 0, DateTimeKind.Unspecified), 72 }, + { 18, 14, 3, new DateTime(2024, 2, 12, 10, 10, 0, 0, DateTimeKind.Unspecified), 36 }, + { 19, 15, 4, new DateTime(2024, 2, 16, 13, 30, 0, 0, DateTimeKind.Unspecified), 84 } + }); + + migrationBuilder.CreateIndex( + name: "IX_cars_model_generation_id", + table: "cars", + column: "model_generation_id"); + + migrationBuilder.CreateIndex( + name: "IX_model_generations_model_id", + table: "model_generations", + column: "model_id"); + + migrationBuilder.CreateIndex( + name: "IX_rentals_car_id", + table: "rentals", + column: "car_id"); + + migrationBuilder.CreateIndex( + name: "IX_rentals_client_id", + table: "rentals", + column: "client_id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "rentals"); + + migrationBuilder.DropTable( + name: "cars"); + + migrationBuilder.DropTable( + name: "clients"); + + migrationBuilder.DropTable( + name: "model_generations"); + + migrationBuilder.DropTable( + name: "car_models"); + } + } +} diff --git a/CarRental/CarRental/CarRental.Infrastructure/Migrations/AppDbContextModelSnapshot.cs b/CarRental/CarRental/CarRental.Infrastructure/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 000000000..47bd37913 --- /dev/null +++ b/CarRental/CarRental/CarRental.Infrastructure/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,886 @@ +// +using System; +using CarRental.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace CarRental.Infrastructure.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.13") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CarRental.Domain.Entities.Car", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("Color") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)") + .HasColumnName("color"); + + b.Property("LicensePlate") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)") + .HasColumnName("license_plate"); + + b.Property("ModelGenerationId") + .HasColumnType("int") + .HasColumnName("model_generation_id"); + + b.HasKey("Id"); + + b.HasIndex("ModelGenerationId"); + + b.ToTable("cars"); + + b.HasData( + new + { + Id = 1, + Color = "Black", + LicensePlate = "A001AA163", + ModelGenerationId = 1 + }, + new + { + Id = 2, + Color = "Red", + LicensePlate = "B777BC163", + ModelGenerationId = 2 + }, + new + { + Id = 3, + Color = "White", + LicensePlate = "C123ET163", + ModelGenerationId = 3 + }, + new + { + Id = 4, + Color = "Green", + LicensePlate = "E555KH163", + ModelGenerationId = 4 + }, + new + { + Id = 5, + Color = "Silver", + LicensePlate = "K234MR163", + ModelGenerationId = 5 + }, + new + { + Id = 6, + Color = "Gray", + LicensePlate = "M888OA163", + ModelGenerationId = 6 + }, + new + { + Id = 7, + Color = "Blue", + LicensePlate = "N456RS163", + ModelGenerationId = 7 + }, + new + { + Id = 8, + Color = "Brown", + LicensePlate = "O789TU163", + ModelGenerationId = 8 + }, + new + { + Id = 9, + Color = "White", + LicensePlate = "P321XO163", + ModelGenerationId = 9 + }, + new + { + Id = 10, + Color = "Black", + LicensePlate = "S654AM163", + ModelGenerationId = 10 + }, + new + { + Id = 11, + Color = "Orange", + LicensePlate = "T987RE163", + ModelGenerationId = 11 + }, + new + { + Id = 12, + Color = "White", + LicensePlate = "U246KN163", + ModelGenerationId = 12 + }, + new + { + Id = 13, + Color = "Black", + LicensePlate = "H135VT163", + ModelGenerationId = 13 + }, + new + { + Id = 14, + Color = "Gray", + LicensePlate = "SH579SA163", + ModelGenerationId = 14 + }, + new + { + Id = 15, + Color = "Blue", + LicensePlate = "SCH864RO163", + ModelGenerationId = 15 + }); + }); + + modelBuilder.Entity("CarRental.Domain.Entities.CarModel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BodyType") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)") + .HasColumnName("body_type"); + + b.Property("Class") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)") + .HasColumnName("class"); + + b.Property("DriveType") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)") + .HasColumnName("drive_type"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)") + .HasColumnName("name"); + + b.Property("SeatsCount") + .HasColumnType("int") + .HasColumnName("seats_count"); + + b.HasKey("Id"); + + b.ToTable("car_models"); + + b.HasData( + new + { + Id = 1, + BodyType = "Sedan", + Class = "Premium", + DriveType = "RWD", + Name = "BMW 3 Series", + SeatsCount = 5 + }, + new + { + Id = 2, + BodyType = "Coupe", + Class = "Sports", + DriveType = "RWD", + Name = "Ford Mustang", + SeatsCount = 4 + }, + new + { + Id = 3, + BodyType = "Sedan", + Class = "Compact", + DriveType = "FWD", + Name = "Honda Civic", + SeatsCount = 5 + }, + new + { + Id = 4, + BodyType = "SUV", + Class = "Off-road", + DriveType = "4WD", + Name = "Jeep Wrangler", + SeatsCount = 5 + }, + new + { + Id = 5, + BodyType = "Coupe", + Class = "Luxury", + DriveType = "RWD", + Name = "Porsche 911", + SeatsCount = 4 + }, + new + { + Id = 6, + BodyType = "SUV", + Class = "Full-size", + DriveType = "AWD", + Name = "Chevrolet Tahoe", + SeatsCount = 8 + }, + new + { + Id = 7, + BodyType = "Sedan", + Class = "Economy", + DriveType = "FWD", + Name = "Lada Vesta", + SeatsCount = 5 + }, + new + { + Id = 8, + BodyType = "SUV", + Class = "Mid-size", + DriveType = "AWD", + Name = "Subaru Outback", + SeatsCount = 5 + }, + new + { + Id = 9, + BodyType = "Van", + Class = "Commercial", + DriveType = "RWD", + Name = "GAZ Gazelle Next", + SeatsCount = 3 + }, + new + { + Id = 10, + BodyType = "Hatchback", + Class = "Hybrid", + DriveType = "FWD", + Name = "Toyota Prius", + SeatsCount = 5 + }, + new + { + Id = 11, + BodyType = "SUV", + Class = "Off-road", + DriveType = "4WD", + Name = "UAZ Patriot", + SeatsCount = 5 + }, + new + { + Id = 12, + BodyType = "SUV", + Class = "Premium", + DriveType = "AWD", + Name = "Lexus RX", + SeatsCount = 5 + }, + new + { + Id = 13, + BodyType = "SUV", + Class = "Luxury", + DriveType = "AWD", + Name = "Range Rover Sport", + SeatsCount = 5 + }, + new + { + Id = 14, + BodyType = "Sedan", + Class = "Premium", + DriveType = "AWD", + Name = "Audi A4", + SeatsCount = 5 + }, + new + { + Id = 15, + BodyType = "SUV", + Class = "Off-road", + DriveType = "4WD", + Name = "Lada Niva Travel", + SeatsCount = 5 + }); + }); + + modelBuilder.Entity("CarRental.Domain.Entities.Client", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("BirthDate") + .HasColumnType("date") + .HasColumnName("birth_date"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)") + .HasColumnName("full_name"); + + b.Property("LicenseNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)") + .HasColumnName("license_number"); + + b.HasKey("Id"); + + b.ToTable("clients"); + + b.HasData( + new + { + Id = 1, + BirthDate = new DateOnly(1988, 3, 15), + FullName = "Alexander Smirnov", + LicenseNumber = "2023-001" + }, + new + { + Id = 2, + BirthDate = new DateOnly(1992, 7, 22), + FullName = "Marina Kovalenko", + LicenseNumber = "2022-045" + }, + new + { + Id = 3, + BirthDate = new DateOnly(1995, 11, 10), + FullName = "Denis Popov", + LicenseNumber = "2024-012" + }, + new + { + Id = 4, + BirthDate = new DateOnly(1985, 5, 3), + FullName = "Elena Vasnetsova", + LicenseNumber = "2021-078" + }, + new + { + Id = 5, + BirthDate = new DateOnly(1990, 9, 30), + FullName = "Igor Kozlovsky", + LicenseNumber = "2023-056" + }, + new + { + Id = 6, + BirthDate = new DateOnly(1993, 2, 14), + FullName = "Anna Orlova", + LicenseNumber = "2022-123" + }, + new + { + Id = 7, + BirthDate = new DateOnly(1987, 8, 18), + FullName = "Artem Belov", + LicenseNumber = "2024-034" + }, + new + { + Id = 8, + BirthDate = new DateOnly(1994, 12, 25), + FullName = "Sofia Grigorieva", + LicenseNumber = "2021-099" + }, + new + { + Id = 9, + BirthDate = new DateOnly(1991, 6, 7), + FullName = "Pavel Melnikov", + LicenseNumber = "2023-087" + }, + new + { + Id = 10, + BirthDate = new DateOnly(1989, 4, 12), + FullName = "Olga Zakharova", + LicenseNumber = "2022-067" + }, + new + { + Id = 11, + BirthDate = new DateOnly(1996, 10, 28), + FullName = "Mikhail Tikhonov", + LicenseNumber = "2024-005" + }, + new + { + Id = 12, + BirthDate = new DateOnly(1986, 1, 19), + FullName = "Ksenia Fedorova", + LicenseNumber = "2021-112" + }, + new + { + Id = 13, + BirthDate = new DateOnly(1997, 7, 3), + FullName = "Roman Sokolov", + LicenseNumber = "2023-092" + }, + new + { + Id = 14, + BirthDate = new DateOnly(1984, 3, 22), + FullName = "Tatiana Krylova", + LicenseNumber = "2022-031" + }, + new + { + Id = 15, + BirthDate = new DateOnly(1998, 11, 15), + FullName = "Andrey Davydov", + LicenseNumber = "2024-021" + }); + }); + + modelBuilder.Entity("CarRental.Domain.Entities.ModelGeneration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("EngineVolume") + .HasColumnType("float") + .HasColumnName("engine_volume"); + + b.Property("ModelId") + .HasColumnType("int") + .HasColumnName("model_id"); + + b.Property("RentalPricePerHour") + .HasColumnType("decimal(18,2)") + .HasColumnName("rental_price_per_hour"); + + b.Property("Transmission") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("nvarchar(10)") + .HasColumnName("transmission"); + + b.Property("Year") + .HasColumnType("int") + .HasColumnName("year"); + + b.HasKey("Id"); + + b.HasIndex("ModelId"); + + b.ToTable("model_generations"); + + b.HasData( + new + { + Id = 1, + EngineVolume = 2.0, + ModelId = 1, + RentalPricePerHour = 2200m, + Transmission = "AT", + Year = 2023 + }, + new + { + Id = 2, + EngineVolume = 5.0, + ModelId = 2, + RentalPricePerHour = 5000m, + Transmission = "AT", + Year = 2022 + }, + new + { + Id = 3, + EngineVolume = 1.5, + ModelId = 3, + RentalPricePerHour = 1200m, + Transmission = "CVT", + Year = 2024 + }, + new + { + Id = 4, + EngineVolume = 3.6000000000000001, + ModelId = 4, + RentalPricePerHour = 2800m, + Transmission = "AT", + Year = 2023 + }, + new + { + Id = 5, + EngineVolume = 3.0, + ModelId = 5, + RentalPricePerHour = 8000m, + Transmission = "AT", + Year = 2024 + }, + new + { + Id = 6, + EngineVolume = 5.2999999999999998, + ModelId = 6, + RentalPricePerHour = 3500m, + Transmission = "AT", + Year = 2022 + }, + new + { + Id = 7, + EngineVolume = 1.6000000000000001, + ModelId = 7, + RentalPricePerHour = 700m, + Transmission = "MT", + Year = 2023 + }, + new + { + Id = 8, + EngineVolume = 2.5, + ModelId = 8, + RentalPricePerHour = 1800m, + Transmission = "AT", + Year = 2024 + }, + new + { + Id = 9, + EngineVolume = 2.7000000000000002, + ModelId = 9, + RentalPricePerHour = 1500m, + Transmission = "MT", + Year = 2022 + }, + new + { + Id = 10, + EngineVolume = 1.8, + ModelId = 10, + RentalPricePerHour = 1600m, + Transmission = "CVT", + Year = 2023 + }, + new + { + Id = 11, + EngineVolume = 2.7000000000000002, + ModelId = 11, + RentalPricePerHour = 1400m, + Transmission = "MT", + Year = 2022 + }, + new + { + Id = 12, + EngineVolume = 3.5, + ModelId = 12, + RentalPricePerHour = 3200m, + Transmission = "AT", + Year = 2024 + }, + new + { + Id = 13, + EngineVolume = 3.0, + ModelId = 13, + RentalPricePerHour = 6000m, + Transmission = "AT", + Year = 2023 + }, + new + { + Id = 14, + EngineVolume = 2.0, + ModelId = 14, + RentalPricePerHour = 2800m, + Transmission = "AT", + Year = 2024 + }, + new + { + Id = 15, + EngineVolume = 1.7, + ModelId = 15, + RentalPricePerHour = 900m, + Transmission = "MT", + Year = 2023 + }); + }); + + modelBuilder.Entity("CarRental.Domain.Entities.Rental", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("CarId") + .HasColumnType("int") + .HasColumnName("car_id"); + + b.Property("ClientId") + .HasColumnType("int") + .HasColumnName("client_id"); + + b.Property("RentalDate") + .HasColumnType("datetime2") + .HasColumnName("rental_date"); + + b.Property("RentalHours") + .HasColumnType("int") + .HasColumnName("rental_hours"); + + b.HasKey("Id"); + + b.HasIndex("CarId"); + + b.HasIndex("ClientId"); + + b.ToTable("rentals"); + + b.HasData( + new + { + Id = 1, + CarId = 7, + ClientId = 1, + RentalDate = new DateTime(2024, 3, 1, 10, 0, 0, 0, DateTimeKind.Unspecified), + RentalHours = 48 + }, + new + { + Id = 2, + CarId = 7, + ClientId = 3, + RentalDate = new DateTime(2024, 2, 25, 14, 30, 0, 0, DateTimeKind.Unspecified), + RentalHours = 72 + }, + new + { + Id = 3, + CarId = 7, + ClientId = 5, + RentalDate = new DateTime(2024, 2, 20, 9, 15, 0, 0, DateTimeKind.Unspecified), + RentalHours = 24 + }, + new + { + Id = 4, + CarId = 1, + ClientId = 2, + RentalDate = new DateTime(2024, 2, 27, 11, 45, 0, 0, DateTimeKind.Unspecified), + RentalHours = 96 + }, + new + { + Id = 5, + CarId = 1, + ClientId = 4, + RentalDate = new DateTime(2024, 2, 25, 16, 0, 0, 0, DateTimeKind.Unspecified), + RentalHours = 120 + }, + new + { + Id = 6, + CarId = 2, + ClientId = 6, + RentalDate = new DateTime(2024, 2, 23, 13, 20, 0, 0, DateTimeKind.Unspecified), + RentalHours = 72 + }, + new + { + Id = 7, + CarId = 2, + ClientId = 8, + RentalDate = new DateTime(2024, 2, 18, 10, 10, 0, 0, DateTimeKind.Unspecified), + RentalHours = 48 + }, + new + { + Id = 8, + CarId = 3, + ClientId = 7, + RentalDate = new DateTime(2024, 2, 28, 8, 30, 0, 0, DateTimeKind.Unspecified), + RentalHours = 36 + }, + new + { + Id = 9, + CarId = 4, + ClientId = 9, + RentalDate = new DateTime(2024, 2, 15, 12, 0, 0, 0, DateTimeKind.Unspecified), + RentalHours = 96 + }, + new + { + Id = 10, + CarId = 5, + ClientId = 10, + RentalDate = new DateTime(2024, 2, 28, 7, 0, 0, 0, DateTimeKind.Unspecified), + RentalHours = 168 + }, + new + { + Id = 11, + CarId = 6, + ClientId = 11, + RentalDate = new DateTime(2024, 2, 22, 15, 45, 0, 0, DateTimeKind.Unspecified), + RentalHours = 72 + }, + new + { + Id = 12, + CarId = 8, + ClientId = 12, + RentalDate = new DateTime(2024, 2, 26, 9, 20, 0, 0, DateTimeKind.Unspecified), + RentalHours = 48 + }, + new + { + Id = 13, + CarId = 9, + ClientId = 13, + RentalDate = new DateTime(2024, 2, 29, 22, 0, 0, 0, DateTimeKind.Unspecified), + RentalHours = 60 + }, + new + { + Id = 14, + CarId = 10, + ClientId = 14, + RentalDate = new DateTime(2024, 2, 24, 11, 30, 0, 0, DateTimeKind.Unspecified), + RentalHours = 96 + }, + new + { + Id = 15, + CarId = 11, + ClientId = 15, + RentalDate = new DateTime(2024, 2, 10, 14, 15, 0, 0, DateTimeKind.Unspecified), + RentalHours = 120 + }, + new + { + Id = 16, + CarId = 12, + ClientId = 1, + RentalDate = new DateTime(2024, 2, 29, 14, 0, 0, 0, DateTimeKind.Unspecified), + RentalHours = 48 + }, + new + { + Id = 17, + CarId = 13, + ClientId = 2, + RentalDate = new DateTime(2024, 2, 5, 16, 45, 0, 0, DateTimeKind.Unspecified), + RentalHours = 72 + }, + new + { + Id = 18, + CarId = 14, + ClientId = 3, + RentalDate = new DateTime(2024, 2, 12, 10, 10, 0, 0, DateTimeKind.Unspecified), + RentalHours = 36 + }, + new + { + Id = 19, + CarId = 15, + ClientId = 4, + RentalDate = new DateTime(2024, 2, 16, 13, 30, 0, 0, DateTimeKind.Unspecified), + RentalHours = 84 + }); + }); + + modelBuilder.Entity("CarRental.Domain.Entities.Car", b => + { + b.HasOne("CarRental.Domain.Entities.ModelGeneration", "ModelGeneration") + .WithMany() + .HasForeignKey("ModelGenerationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ModelGeneration"); + }); + + modelBuilder.Entity("CarRental.Domain.Entities.ModelGeneration", b => + { + b.HasOne("CarRental.Domain.Entities.CarModel", "Model") + .WithMany() + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Model"); + }); + + modelBuilder.Entity("CarRental.Domain.Entities.Rental", b => + { + b.HasOne("CarRental.Domain.Entities.Car", "Car") + .WithMany() + .HasForeignKey("CarId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("CarRental.Domain.Entities.Client", "Client") + .WithMany() + .HasForeignKey("ClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Car"); + + b.Navigation("Client"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CarRental/CarRental/CarRental.Infrastructure/Persistence/AppDbContext.cs b/CarRental/CarRental/CarRental.Infrastructure/Persistence/AppDbContext.cs new file mode 100644 index 000000000..d19c310c7 --- /dev/null +++ b/CarRental/CarRental/CarRental.Infrastructure/Persistence/AppDbContext.cs @@ -0,0 +1,62 @@ +using CarRental.Domain.Data; +using CarRental.Domain.Entities; +using Microsoft.EntityFrameworkCore; + +namespace CarRental.Infrastructure.Persistence; + +public class AppDbContext(DbContextOptions options, CarRentalFixture fixture) : DbContext(options) +{ + public DbSet Cars { get; set; } + public DbSet Clients { get; set; } + public DbSet CarModels { get; set; } + public DbSet ModelGenerations { get; set; } + public DbSet Rentals { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(entity => + { + entity.HasKey(c => c.Id); + entity.HasOne(c => c.ModelGeneration) + .WithMany() + .HasForeignKey(c => c.ModelGenerationId) + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(mg => mg.Id); + entity.HasOne(mg => mg.Model) + .WithMany() + .HasForeignKey(mg => mg.ModelId) + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(r => r.Id); + entity.HasOne(r => r.Car) + .WithMany() + .HasForeignKey(r => r.CarId) + .OnDelete(DeleteBehavior.Cascade); + entity.HasOne(r => r.Client) + .WithMany() + .HasForeignKey(r => r.ClientId) + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity() + .HasKey(c => c.Id); + + modelBuilder.Entity() + .HasKey(cm => cm.Id); + + modelBuilder.Entity().HasData(fixture.Cars); + modelBuilder.Entity().HasData(fixture.CarModels); + modelBuilder.Entity().HasData(fixture.Clients); + modelBuilder.Entity().HasData(fixture.ModelGenerations); + modelBuilder.Entity().HasData(fixture.Rentals); + } +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Infrastructure/Repositories/DbRepository.cs b/CarRental/CarRental/CarRental.Infrastructure/Repositories/DbRepository.cs new file mode 100644 index 000000000..02620b4e0 --- /dev/null +++ b/CarRental/CarRental/CarRental.Infrastructure/Repositories/DbRepository.cs @@ -0,0 +1,62 @@ +using CarRental.Domain.Interfaces; +using CarRental.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace CarRental.Infrastructure.Repositories; + +public class DbRepository(AppDbContext context) : IRepository where T : class +{ + protected readonly DbSet _set = context.Set(); + + public async Task GetByIdAsync(int id) => await _set.FindAsync(id); + + public async Task> GetAllAsync() => await _set.ToListAsync(); + + public async Task AddAsync(T entity) + { + await _set.AddAsync(entity); + await context.SaveChangesAsync(); + return entity; + } + + public async Task UpdateAsync(T entity) + { + _set.Update(entity); + await context.SaveChangesAsync(); + return entity; + } + + public async Task DeleteAsync(int id) + { + var entity = await _set.FindAsync(id); + if (entity == null) return false; + _set.Remove(entity); + await context.SaveChangesAsync(); + return true; + } + + public async Task GetByIdAsync(int id, Func, IQueryable>? include = null) + { + var query = _set.AsQueryable(); + if (include != null) + query = include(query); + + return await query.FirstOrDefaultAsync(e => EF.Property(e, "Id") == id); + } + + public async Task> GetAllAsync(Func, IQueryable>? include = null) + { + var query = _set.AsQueryable(); + if (include != null) + query = include(query); + return await query.ToListAsync(); + } + + public IQueryable GetQueryable(Func, IQueryable>? include = null) + { + var query = _set.AsQueryable(); + if (include != null) + query = include(query); + return query; + } +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.ServiceDefaults/CarRental.ServiceDefaults.csproj b/CarRental/CarRental/CarRental.ServiceDefaults/CarRental.ServiceDefaults.csproj new file mode 100644 index 000000000..2afa7684e --- /dev/null +++ b/CarRental/CarRental/CarRental.ServiceDefaults/CarRental.ServiceDefaults.csproj @@ -0,0 +1,18 @@ + + + net8.0 + enable + enable + true + + + + + + + + + + + + \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.ServiceDefaults/Extensions.cs b/CarRental/CarRental/CarRental.ServiceDefaults/Extensions.cs new file mode 100644 index 000000000..87618230d --- /dev/null +++ b/CarRental/CarRental/CarRental.ServiceDefaults/Extensions.cs @@ -0,0 +1,88 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ServiceDiscovery; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace Microsoft.Extensions.Hosting; + +public static class Extensions +{ + public static TBuilder AddServiceDefaults(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.ConfigureOpenTelemetry(); + builder.AddDefaultHealthChecks(); + builder.Services.AddServiceDiscovery(); + + builder.Services.ConfigureHttpClientDefaults(http => + { + http.AddStandardResilienceHandler(); + http.AddServiceDiscovery(); + }); + + return builder; + } + + public static TBuilder ConfigureOpenTelemetry(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Logging.AddOpenTelemetry(logging => + { + logging.IncludeFormattedMessage = true; + logging.IncludeScopes = true; + }); + + builder.Services.AddOpenTelemetry() + .WithMetrics(metrics => + { + metrics.AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddRuntimeInstrumentation(); + }) + .WithTracing(tracing => + { + tracing.AddSource(builder.Environment.ApplicationName) + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation(); + }); + + builder.AddOpenTelemetryExporters(); + + return builder; + } + + private static TBuilder AddOpenTelemetryExporters(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + var useOtlpExporter = !string.IsNullOrWhiteSpace(builder.Configuration["OTEL_EXPORTER_OTLP_ENDPOINT"]); + if (useOtlpExporter) + { + builder.Services.AddOpenTelemetry().UseOtlpExporter(); + } + + return builder; + } + + public static TBuilder AddDefaultHealthChecks(this TBuilder builder) where TBuilder : IHostApplicationBuilder + { + builder.Services.AddHealthChecks() + .AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]); + return builder; + } + + public static WebApplication MapDefaultEndpoints(this WebApplication app) + { + if (app.Environment.IsDevelopment()) + { + app.MapHealthChecks("/health"); + app.MapHealthChecks("/alive", new HealthCheckOptions + { + Predicate = r => r.Tags.Contains("live") + }); + } + + return app; + } +} \ No newline at end of file diff --git a/CarRental/CarRental/CarRental.Tests/CarRental.Tests.csproj b/CarRental/CarRental/CarRental.Tests/CarRental.Tests.csproj new file mode 100644 index 000000000..7361aa8b2 --- /dev/null +++ b/CarRental/CarRental/CarRental.Tests/CarRental.Tests.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + diff --git a/CarRental/CarRental/CarRental.Tests/CarRentalTests.cs b/CarRental/CarRental/CarRental.Tests/CarRentalTests.cs new file mode 100644 index 000000000..7788d9bd7 --- /dev/null +++ b/CarRental/CarRental/CarRental.Tests/CarRentalTests.cs @@ -0,0 +1,190 @@ +using CarRental.Domain.Data; + +namespace CarRental.Tests; + + +/// +/// Юнит-тесты для пункта проката автомобилей +/// +public class CarRentalTests(CarRentalFixture fixture) : IClassFixture +{ + /// + /// ТЕСТ 1: Вывести информацию обо всех клиентах, + /// которые брали в аренду автомобили указанной модели, упорядочить по ФИО. + /// + [Fact] + public void GetClientsByModelSortedByName() + { + const string targetModel = "Lada Vesta"; + const int expectedCount = 3; + const string expectedFirstName = "Alexander Smirnov"; + const string expectedSecondName = "Denis Popov"; + const string expectedThirdName = "Igor Kozlovsky"; + + var modelId = fixture.CarModels.FirstOrDefault(m => m.Name == targetModel)?.Id; + + var generationIds = fixture.ModelGenerations + .Where(mg => mg.ModelId == modelId) + .Select(mg => mg.Id) + .ToList(); + + var carIds = fixture.Cars + .Where(c => generationIds.Contains(c.ModelGenerationId)) + .Select(c => c.Id) + .ToList(); + + var clientIds = fixture.Rentals + .Where(r => carIds.Contains(r.CarId)) + .Select(r => r.ClientId) + .Distinct() + .ToList(); + + var clients = fixture.Clients + .Where(c => clientIds.Contains(c.Id)) + .OrderBy(c => c.FullName) + .ToList(); + + Assert.Equal(expectedCount, clients.Count); + Assert.Equal(expectedFirstName, clients[0].FullName); + Assert.Equal(expectedSecondName, clients[1].FullName); + Assert.Equal(expectedThirdName, clients[2].FullName); + } + + /// + /// ТЕСТ 2: Вывести информацию об автомобилях, находящихся в аренде. + /// + [Fact] + public void GetCurrentlyRentedCars() + { + var testDate = new DateTime(2024, 3, 5, 12, 0, 0); + var expectedPlate = "K234MR163"; + + var rentedCarIds = fixture.Rentals + .Where(r => r.RentalDate.AddHours(r.RentalHours) > testDate) + .Select(r => r.CarId) + .Distinct() + .ToList(); + + var rentedCars = fixture.Cars + .Where(c => rentedCarIds.Contains(c.Id)) + .ToList(); + + Assert.Contains(rentedCars, c => c.LicensePlate == expectedPlate); + } + + /// + /// ТЕСТ 3: Вывести топ 5 наиболее часто арендуемых автомобилей. + /// + [Fact] + public void GetTop5MostRentedCars() + { + const int expectedCount = 5; + const string expectedTopCarPlate = "N456RS163"; + const int expectedTopCarRentalCount = 3; + + var topCarStats = fixture.Rentals + .GroupBy(r => r.CarId) + .Select(g => new { CarId = g.Key, RentalCount = g.Count() }) + .OrderByDescending(x => x.RentalCount) + .Take(5) + .ToList(); + + var topCarIds = topCarStats.Select(x => x.CarId).ToList(); + var carsDict = fixture.Cars + .Where(c => topCarIds.Contains(c.Id)) + .ToDictionary(c => c.Id); + + var topCars = topCarStats + .Select(x => new + { + Car = carsDict.GetValueOrDefault(x.CarId), + x.RentalCount + }) + .Where(x => x.Car != null) + .ToList(); + + Assert.Equal(expectedCount, topCars.Count); + Assert.Equal(expectedTopCarPlate, topCars[0].Car?.LicensePlate); + Assert.Equal(expectedTopCarRentalCount, topCars[0].RentalCount); + } + + /// + /// ТЕСТ 4: Для каждого автомобиля вывести число аренд. + /// + [Fact] + public void GetRentalCountPerCar() + { + const int expectedTotalCars = 15; + const int expectedLadaVestaRentalCount = 3; + const int expectedBmwRentalCount = 2; + const int ladaVestaCarId = 7; + const int bmwCarId = 1; + + var rentalCounts = fixture.Rentals + .GroupBy(r => r.CarId) + .ToDictionary(g => g.Key, g => g.Count()); + + var carsWithRentalCount = fixture.Cars + .Select(car => new + { + Car = car, + RentalCount = rentalCounts.GetValueOrDefault(car.Id, 0) + }) + .ToList(); + + Assert.Equal(expectedTotalCars, carsWithRentalCount.Count); + + var ladaVesta = carsWithRentalCount.First(c => c.Car.Id == ladaVestaCarId); + var bmw = carsWithRentalCount.First(c => c.Car.Id == bmwCarId); + + Assert.Equal(expectedLadaVestaRentalCount, ladaVesta.RentalCount); + Assert.Equal(expectedBmwRentalCount, bmw.RentalCount); + Assert.True(carsWithRentalCount.All(x => x.RentalCount >= 0)); + } + + /// + /// ТЕСТ 5: Вывести топ 5 клиентов по сумме аренды. + /// + [Fact] + public void GetTop5ClientsByRentalAmount() + { + const int expectedCount = 5; + const string expectedTopClientName = "Olga Zakharova"; + + var carPrices = fixture.Cars + .Join(fixture.ModelGenerations, + c => c.ModelGenerationId, + g => g.Id, + (c, g) => new { CarId = c.Id, Price = g.RentalPricePerHour }) + .ToDictionary(x => x.CarId, x => x.Price); + + var topClientStats = fixture.Rentals + .GroupBy(r => r.ClientId) + .Select(g => new + { + ClientId = g.Key, + TotalAmount = g.Sum(r => r.RentalHours * carPrices.GetValueOrDefault(r.CarId, 0)) + }) + .OrderByDescending(x => x.TotalAmount) + .Take(5) + .ToList(); + + var topClientIds = topClientStats.Select(x => x.ClientId).ToList(); + var clientsDict = fixture.Clients + .Where(c => topClientIds.Contains(c.Id)) + .ToDictionary(c => c.Id); + + var topClients = topClientStats + .Select(x => new + { + Client = clientsDict.GetValueOrDefault(x.ClientId), + x.TotalAmount + }) + .Where(x => x.Client != null) + .ToList(); + + Assert.Equal(expectedCount, topClients.Count); + Assert.Equal(expectedTopClientName, topClients[0].Client?.FullName); + } +} + diff --git a/CarRental/CarRental/CarRental.slnx b/CarRental/CarRental/CarRental.slnx new file mode 100644 index 000000000..933fe8c5a --- /dev/null +++ b/CarRental/CarRental/CarRental.slnx @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/README.md b/README.md index 76afcbfdd..5cce7c175 100644 --- a/README.md +++ b/README.md @@ -1,137 +1,37 @@ -# Разработка корпоративных приложений -[Таблица с успеваемостью](https://docs.google.com/spreadsheets/d/1JD6aiOG6r7GrA79oJncjgUHWtfeW4g_YZ9ayNgxb_w0/edit?usp=sharing) +# Лабораторная работа 2 и 3: Сервер - Реализация серверного приложения с использованием REST API, +# ORM - Реализация объектно-реляционной модели. Подключение к базе данных и настройка оркестрации -## Задание -### Цель -Реализация проекта сервисно-ориентированного приложения. -### Задачи -* Реализация объектно-ориентированной модели данных, -* Изучение реализации серверных приложений на базе WebAPI/OpenAPI, -* Изучение работы с брокерами сообщений, -* Изучение паттернов проектирования, -* Изучение работы со средствами оркестрации на примере .NET Aspire, -* Повторение основ работы с системами контроля версий, -* Unit-тестирование. +## Вариант 80 -### Лабораторные работы -
-1. «Классы» - Реализация объектной модели данных и unit-тестов -
-В рамках первой лабораторной работы необходимо подготовить структуру классов, описывающих предметную область, определяемую в задании. В каждом из заданий присутствует часть, связанная с обработкой данных, представленная в разделе «Unit-тесты». Данную часть необходимо реализовать в виде unit-тестов: подготовить тестовые данные, выполнить запрос с использованием LINQ, проверить результаты. +* **Platform**: .NET 8 (C# 12) +* **Database**: SqlServer +* **ORM**: Entity Framework Core 8 +* **Messaging**: Apache Kafka +* **Mapping**: AutoMapper +* **Testing**: xUnit, Bogus (Fake Data) -Хранение данных на этом этапе допускается осуществлять в памяти в виде коллекций. -Необходимо включить **как минимум 10** экземпляров каждого класса в датасид. - -
-
-2. «Сервер» - Реализация серверного приложения с использованием REST API -
-Во второй лабораторной работе необходимо реализовать серверное приложение, которое должно: -- Осуществлять базовые CRUD-операции с реализованными в первой лабораторной сущностями -- Предоставлять результаты аналитических запросов (раздел «Unit-тесты» задания) -Хранение данных на этом этапе допускается осуществлять в памяти в виде коллекций. -
-
-
-3. «ORM» - Реализация объектно-реляционной модели. Подключение к базе данных и настройка оркестрации -
-В третьей лабораторной работе хранение должно быть переделано c инмемори коллекций на базу данных. -Должны быть созданы миграции для создания таблиц в бд и их первоначального заполнения. -
-Также необходимо настроить оркестратор Aspire на запуск сервера и базы данных. -
-
-
-4. «Инфраструктура» - Реализация сервиса генерации данных и его интеграция с сервером -
-В четвертой лабораторной работе необходимо имплементировать сервис, который генерировал бы контракты. Контракты далее передаются в сервер и сохраняются в бд. -Сервис должен представлять из себя отдельное приложение без референсов к серверным проектам за исключением библиотеки с контрактами. -Отправка контрактов при помощи gRPC должна выполняться в потоковом виде. -При использовании брокеров сообщений, необходимо предусмотреть ретраи при подключении к брокеру. +## Описание предметной области -Также необходимо добавить в конфигурацию Aspire запуск генератора и (если того требует вариант) брокера сообщений. -
-
-
-5. «Клиент» - Интеграция клиентского приложения с оркестратором -
-В пятой лабораторной необходимо добавить в конфигурацию Aspire запуск клиентского приложения для написанного ранее сервера. Клиент создается в рамках курса "Веб разработка". -
-
+Реализована объектная модель для пункта проката автомобилей со следующими сущностями: -## Задание. Общая часть -**Обязательно**: -* Реализация серверной части на [.NET 8](https://learn.microsoft.com/ru-ru/dotnet/core/whats-new/dotnet-8/overview). -* Реализация серверной части на [ASP.NET](https://dotnet.microsoft.com/ru-ru/apps/aspnet). -* Реализация unit-тестов с использованием [xUnit](https://xunit.net/?tabs=cs). -* Использование хранения данных в базе данных согласно варианту задания. -* Оркестрация проектов при помощи [.NET Aspire](https://learn.microsoft.com/ru-ru/dotnet/aspire/get-started/aspire-overview) -* Реализация сервиса генерации данных при помощи [Bogus](https://github.com/bchavez/Bogus) и его взаимодейсвие с сервером согласно варианту задания. -* Автоматизация тестирования на уровне репозитория через [GitHub Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions). -* Создание минимальной документации к проекту: страница на GitHub с информацией о задании, скриншоты приложения и прочая информация. +- **`CarModel`** – модель автомобиля (справочник): тип привода, класс, тип кузова, количество мест. +- **`ModelGeneration`** – поколение модели: год выпуска, объём двигателя, коробка передач, стоимость часа аренды. +- **`Car`** – физический экземпляр автомобиля: госномер, цвет, поколение модели. +- **`Client`** – клиент: номер водительского удостоверения, ФИО, дата рождения. +- **`Rental`** – договор аренды: клиент, автомобиль, время выдачи, длительность в часах. -**Факультативно**: -* Реализация авторизации/аутентификации. -* Реализация atomic batch publishing/atomic batch consumption для брокеров, поддерживающих такой функционал. -* Реализация интеграционных тестов при помощи .NET Aspire. -* Реализация клиента на Blazor WASM. +## Реализованные тесты (xUnit) -Внимательно прочитайте [дискуссии](https://github.com/itsecd/enterprise-development/discussions/1) о том, как работает автоматическое распределение на ревью. -Сразу корректно называйте свои pr, чтобы они попали на ревью нужному преподавателю. +1. **`GetClientsByModelSortedByName`** – клиенты, бравшие в аренду указанную модель, отсортированные по ФИО. +2. **`GetCurrentlyRentedCars`** – автомобили, которые сейчас находятся в аренде (нет даты возврата). +3. **`GetTop5MostRentedCars`**` – топ‑5 автомобилей по количеству аренд. +4. **`GetRentalCountPerCar`** – количество аренд для каждого автомобиля. +5. **`GetTop5ClientsByRentalAmount`** – топ‑5 клиентов по сумме стоимости аренды. -По итогу работы в семестре должна получиться следующая информационная система: -
-C4 диаграмма +## Результат 2 и 3 лабораторной работы -image1 - -
- -## Варианты заданий -Номер варианта задания присваивается в начале семестра. Изменить его нельзя. Каждый вариант имеет уникальную комбинацию из предметной области, базы данных и технологии для общения сервиса генерации данных и сервера апи. - -[Список вариантов](https://docs.google.com/document/d/1Wc8AvsKS_1JptpsxHO-cwfAxz2ghxvQRQ0fy4el2ZOc/edit?usp=sharing) -[Список предметных областей](https://docs.google.com/document/d/15jWhXMwd2K8giFMKku_yrY_s2uQNEu4ugJXLYPvYJAE/edit?usp=sharing) -[Вопросы к экзамену](https://docs.google.com/document/d/1bjfvtzjyMljJbcu8YCvC8DzDegDUAmDeNtBz9M6FQes/edit?usp=sharing) - -## Схема сдачи - -На каждую из лабораторных работ необходимо сделать отдельный [Pull Request (PR)](https://docs.github.com/en/pull-requests). - -Общая схема: -1. Сделать форк данного репозитория -2. Выполнить задание -3. Сделать PR в данный репозиторий -4. Исправить замечания после code review -5. Получить approve -6. Прийти на занятие и защитить работу - -## Критерии оценивания - -Конкурентный принцип. -Так как задания в первой лабораторной будут повторяться между студентами, то выделяются следующие показатели для оценки: -1. Скорость разработки -2. Качество разработки -3. Полнота выполнения задания - -Быстрее делаете PR - у вас преимущество. -Быстрее получаете Approve - у вас преимущество. -Выполните нечто немного выходящее за рамки проекта - у вас преимущество. - -### Шкала оценивания - -- **3 балла** за качество кода, из них: - - 2 балла - базовая оценка - - 1 балл (но не более) можно получить за выполнение любого из следующих пунктов: - - Реализация факультативного функционала - - Выполнение работы раньше других: первые 5 человек из каждой группы, которые сделали PR и получили approve, получают дополнительный балл -- **3 балла** за защиту: при сдаче лабораторной работы вам задается 3 вопроса, за каждый правильный ответ - 1 балл - -У вас 2 попытки пройти ревью (первичное ревью, ревью по результатам исправления). Если замечания по итогу не исправлены, то снимается один балл за код лабораторной работы. - -## Вопросы и обратная связь по курсу - -Чтобы задать вопрос по лабораторной, воспользуйтесь [соотвествующим разделом дискуссий](https://github.com/itsecd/enterprise-development/discussions/categories/questions) или заведите [ишью](https://github.com/itsecd/enterprise-development/issues/new). -Если у вас появились идеи/пожелания/прочие полезные мысли по преподаваемой дисциплине, их можно оставить [здесь](https://github.com/itsecd/enterprise-development/discussions/categories/ideas). +- Реализовано серверное приложение, которое осуществляет базовые CRUD-операции с реализованными в первой лабораторной сущностями и предоставляет результаты аналитических запросов +- Созданы миграции для создания таблиц в бд и их первоначального заполнения. +- Также был создан оркестратор Aspire на запуск сервера и базы данных.