Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions CarRental/CarRental/.github/workflows/dotnet-tests.yml
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions CarRental/CarRental/CarRental.API/CarRental.API.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Microsoft.EntityFrameworkCore.SqlServer" Version="13.0.2" />
<PackageReference Include="AutoMapper" Version="15.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\CarRental.Application.Contracts\CarRental.Application.Contracts.csproj" />
<ProjectReference Include="..\CarRental.Infrastructure\CarRental.Infrastructure.csproj" />
<ProjectReference Include="..\CarRental.ServiceDefaults\CarRental.ServiceDefaults.csproj" />
</ItemGroup>
</Project>
199 changes: 199 additions & 0 deletions CarRental/CarRental/CarRental.API/Controllers/AnalyticsController.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Контроллер для аналитических запросов и отчетов
/// </summary>
[ApiController]
[Route("api/analytics")]
public class AnalyticsController(
IRepository<Rental> rentalsRepo,
IRepository<Car> carsRepo,
IRepository<Client> clientsRepo,
IRepository<ModelGeneration> generationsRepo,
IMapper mapper) : ControllerBase
{
/// <summary>
/// Получает список клиентов, арендовавших автомобили указанной модели, отсортированный по названию
/// </summary>
/// <param name="modelName">Название модели автомобиля</param>
/// <returns>Список клиентов</returns>
[HttpGet("clients-by-model")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<ClientGetDto>>> 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<ClientGetDto>)
.ToList();

return Ok(result);
}

/// <summary>
/// Получает арендованные в данный момент автомобили
/// </summary>
/// <param name="currentDate">Текущая дата проверки</param>
/// <returns>Список арендованных автомобилей</returns>
[HttpGet("currently-rented-cars")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<CarGetDto>>> 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<CarGetDto>)
.ToList();

return Ok(result);
}

/// <summary>
/// Получает топ 5 самых популярных арендованных автомобилей
/// </summary>
/// <returns>Список автомобилей, которые можно взять напрокат</returns>
[HttpGet("top-5-most-rented-cars")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<CarRentalCountDto>>> 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<CarGetDto>(carsDict[x.CarId]),
x.RentalCount))
.ToList();

return Ok(topCarsResult);
}

/// <summary>
/// Получает количество арендованных автомобилей для каждого автомобиля
/// </summary>
/// <returns>Список всех автомобилей, которые были взяты в аренду</returns>
[HttpGet("rental-count-per-car")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<CarRentalCountDto>>> 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<CarGetDto>(car),
rentalCounts.GetValueOrDefault(car.Id, 0)))
.OrderByDescending(x => x.RentalCount)
.ToList();

return Ok(carsWithRentalCount);
}

/// <summary>
/// Получает топ 5 клиентов по общей сумме аренды
/// </summary>
/// <returns>Список клиентов с общей суммой арендной платы</returns>
[HttpGet("top-5-clients-by-rental-amount")]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<ClientRentalAmountDto>>> 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<ClientGetDto>(clientsDict[x.ClientId]),
x.TotalAmount))
.ToList();

return Ok(result);
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Контроллер для управления моделями автомобилей
/// </summary>
[ApiController]
[Route("api/car-models")]
public class CarModelsController(
IRepository<CarModel> repo,
IMapper mapper) : ControllerBase
{
/// <summary>
/// Получает доступ ко всем моделям автомобилей
/// </summary>
/// <returns>Список всех моделей автомобилей</returns>
[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<CarModelGetDto>>> GetAll()
{
var entities = await repo.GetAllAsync();
var dtos = mapper.Map<IEnumerable<CarModelGetDto>>(entities);
return Ok(dtos);
}

/// <summary>
/// Получает модель автомобиля по идентификатору
/// </summary>
/// <param name="id">Идентификатор модели</param>
/// <returns>Модель автомобиля</returns>
[HttpGet("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<CarModelGetDto>> Get(int id)
{
var entity = await repo.GetByIdAsync(id);
if (entity == null) return NotFound();
var dto = mapper.Map<CarModelGetDto>(entity);
return Ok(dto);
}

/// <summary>
/// Создает новую модель автомобиля
/// </summary>
/// <param name="dto">Данные для создания модели</param>
/// <returns>Созданная модель автомобиля</returns>
[HttpPost]
[ProducesResponseType(StatusCodes.Status201Created)]
public async Task<ActionResult<CarModelGetDto>> Create([FromBody] CarModelEditDto dto)
{
var entity = mapper.Map<CarModel>(dto);
var created = await repo.AddAsync(entity);
var resultDto = mapper.Map<CarModelGetDto>(created);
return CreatedAtAction(nameof(Get), new { id = resultDto.Id }, resultDto);
}

/// <summary>
/// Обновляет существующую модель автомобиля
/// </summary>
/// <param name="id">Идентификатор модели</param>
/// <param name="dto">Обновленные данные модели</param>
/// <returns>Обновленная модель автомобиля</returns>
[HttpPut("{id}")]
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<ActionResult<CarModelGetDto>> 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<CarModelGetDto>(entity);
return Ok(resultDto);
}

/// <summary>
/// Удалить модель автомобиля
/// </summary>
/// <param name="id">Идентификатор модели</param>
[HttpDelete("{id}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
public async Task<ActionResult> Delete(int id)
{
await repo.DeleteAsync(id);
return NoContent();
}
}
Loading