Сделал все

This commit is contained in:
2025-04-22 21:43:25 +04:00
parent ddc09bc0f6
commit d687438fe4
14 changed files with 323 additions and 3 deletions

View File

@@ -12,13 +12,14 @@ namespace TwoFromTheCasketBusinessLogic.Implementation;
internal class ReportContract(
IRoomStorageContract roomStorageContract, IRoomHistoryStorageContract roomHistoryStorageContract,
IComplitedWorkStorageContract complitedWorkStorageContract, IWorkStorageContract workStorageContract,
IWorkerStorageContract workerStorageContract,
IWorkerStorageContract workerStorageContract, ISalaryStorageContract salaryStorageContract,
ILogger logger, BaseWordBuilder baseWordBuilder,
BaseExcelBuilder baseExcelBuilder, BasePdfBuilder basePdfBuilder) : IReportContract
{
private readonly IRoomStorageContract _roomStorageContract = roomStorageContract;
private readonly IRoomHistoryStorageContract _roomHistoryStorageContract = roomHistoryStorageContract;
private readonly IComplitedWorkStorageContract _complitedWorkStorageContract = complitedWorkStorageContract;
private readonly ISalaryStorageContract _salaryStorageContract = salaryStorageContract;
private readonly IWorkStorageContract _workStorageContract = workStorageContract;
private readonly IWorkerStorageContract _workerStorageContract = workerStorageContract;
private readonly ILogger _logger = logger;
@@ -158,5 +159,46 @@ internal class ReportContract(
.Build();
}
public async Task<List<SalaryByWorkerReportViewModel>> GetWorkerSalariesByPeriodAsync(string workerId, DateTime from, DateTime to, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(workerId))
throw new ArgumentNullException(nameof(workerId), "Worker ID cannot be null or empty.");
if (!Guid.TryParse(workerId, out _))
throw new ValidationException("Invalid worker ID format.");
if (from >= to)
throw new IncorrectDatesException(from, to);
var salaries = await _salaryStorageContract.GetListAsync(from, to, ct, workerId)
?? throw new NullReferenceException("No salary data found.");
var worker = await _workerStorageContract.GetElementByIdAsync(workerId, ct)
?? throw new NullReferenceException("Worker not found.");
return salaries.Select(s => new SalaryByWorkerReportViewModel
{
Date = s.Date,
WorkerFIO = worker.FIO,
Amount = s.Sum
}).ToList();
}
public async Task<Stream> CreateDocumentSalaryByWorkerAsync(string workerId, DateTime from, DateTime to, CancellationToken ct)
{
_logger.LogInformation("Create report SalaryByWorker from {from} to {to} for worker {workerId}", from, to, workerId);
var data = await GetWorkerSalariesByPeriodAsync(workerId, from, to, ct)
?? throw new InvalidOperationException("No salary data found");
if (!data.Any())
throw new InvalidOperationException("No salary data found for the selected worker and period");
string fio = data.First().WorkerFIO;
return _basePdfBuilder
.AddHeader("Зарплатная ведомость")
.AddParagraph($"за период с {from.ToShortDateString()} по {to.ToShortDateString()}")
.AddPieChart("Начисления", [.. data.Select(x => (x.Date.ToShortDateString(), x.Amount))])
.Build();
}
}

View File

@@ -10,4 +10,6 @@ public interface IReportAdapter
Task<ReportOperationResponse> CreateDocumentRoomHistoryAsync(CancellationToken ct);
Task<ReportOperationResponse> CreateDocumentComplitedWorksAsync(DateTime from, DateTime to, CancellationToken ct);
Task<ReportOperationResponse> GetComplitedWorksByPeriodAsync(DateTime from, DateTime to, CancellationToken ct);
Task<ReportOperationResponse> GetWorkerSalariesByPeriodAsync(string workerId, DateTime from, DateTime to, CancellationToken ct);
Task<ReportOperationResponse> CreateDocumentSalaryByWorkerAsync(string workerId, DateTime from, DateTime to, CancellationToken ct);
}

View File

@@ -8,6 +8,7 @@ public class ReportOperationResponse : OperationResponse
{
public static ReportOperationResponse OK(List<RoomWithHistoryViewModel> data) => OK<ReportOperationResponse, List<RoomWithHistoryViewModel>>(data);
public static ReportOperationResponse OK(List<ComplitedWorkReportViewModel> data) => OK<ReportOperationResponse, List<ComplitedWorkReportViewModel>>(data);
public static ReportOperationResponse OK(List<SalaryByWorkerReportViewModel> data) => OK<ReportOperationResponse, List<SalaryByWorkerReportViewModel>>(data);
public static ReportOperationResponse OK(Stream data, string filename) => OK<ReportOperationResponse, Stream>(data, filename);
public static ReportOperationResponse BadRequest(string message) => BadRequest<ReportOperationResponse>(message);

View File

@@ -9,6 +9,8 @@ public interface IReportContract
{
Task<List<RoomWithHistoryDataModel>> GetRoomHistoryGroupedAsync(CancellationToken ct);
Task<List<ComplitedWorkReportViewModel>> GetComplitedWorksByPeriodAsync(DateTime from, DateTime to, CancellationToken ct);
Task<List<SalaryByWorkerReportViewModel>> GetWorkerSalariesByPeriodAsync(string workerId, DateTime from, DateTime to, CancellationToken ct);
Task<Stream> CreateDocumentRoomHistoryAsync(CancellationToken ct);
Task<Stream> CreateDocumentComplitedWorksAsync(DateTime from, DateTime to, CancellationToken ct);
Task<Stream> CreateDocumentSalaryByWorkerAsync(string workerId, DateTime from, DateTime to, CancellationToken ct);
}

View File

@@ -0,0 +1,8 @@
namespace TwoFromTheCasketContracts.DataModels;
public class SalaryByWorkerReportDataModel
{
public required string WorkerFIO { get; set; }
public DateTime Date { get; set; }
public double Amount { get; set; }
}

View File

@@ -3,6 +3,7 @@ namespace TwoFromTheCasketContracts.StorageContracts;
public interface ISalaryStorageContract
{
List<SalaryDataModel> GetList(DateTime startDate, DateTime endDate, string? workerId = null);
Task<List<SalaryDataModel>> GetListAsync(DateTime startDate, DateTime endDate, CancellationToken ct, string? workerId = null);
void AddElement(SalaryDataModel salaryDataModel);
}

View File

@@ -5,6 +5,7 @@ public interface IWorkerStorageContract
List<WorkerDataModel> GetList(bool onlyActive = true, string? SpecializationId = null, DateTime? fromBirthDate = null, DateTime? toBirthDate = null, DateTime? fromEmploymentDate = null, DateTime? toEmploymentDate = null);
Task<List<WorkerDataModel>> GetListAsync(CancellationToken ct);
WorkerDataModel? GetElementById(string id);
Task<WorkerDataModel?> GetElementByIdAsync(string id, CancellationToken ct);
WorkerDataModel? GetElementByFIO(string FIO);
WorkerDataModel? GetElementByPhoneNumber(string PhoneNumber);
void AddElement(WorkerDataModel workerDataModel);

View File

@@ -0,0 +1,8 @@
namespace TwoFromTheCasketContracts.ViewModels;
public class SalaryByWorkerReportViewModel
{
public required string WorkerFIO { get; set; }
public DateTime Date { get; set; }
public double Amount { get; set; }
}

View File

@@ -41,6 +41,34 @@ public class SalaryStorageContract : ISalaryStorageContract
throw new StorageException(e);
}
}
public async Task<List<SalaryDataModel>> GetListAsync(DateTime startDate, DateTime endDate, CancellationToken ct, string? workerId)
{
try
{
if (startDate.Kind != DateTimeKind.Utc)
startDate = DateTime.SpecifyKind(startDate, DateTimeKind.Utc);
if (endDate.Kind != DateTimeKind.Utc)
endDate = DateTime.SpecifyKind(endDate, DateTimeKind.Utc);
var query = _dbContext.Salaries
.Where(x => x.Date >= startDate && x.Date <= endDate);
if (!string.IsNullOrEmpty(workerId))
query = query.Where(x => x.WorkerId == workerId);
var result = await query
.Select(x => _mapper.Map<SalaryDataModel>(x))
.ToListAsync(ct);
return result;
}
catch (Exception e)
{
_dbContext.ChangeTracker.Clear();
throw new StorageException(e);
}
}
public void AddElement(SalaryDataModel salaryDataModel)
{
@@ -71,5 +99,4 @@ public class SalaryStorageContract : ISalaryStorageContract
throw new StorageException(ex);
}
}
}

View File

@@ -95,6 +95,21 @@ public class WorkerStorageContract : IWorkerStorageContract
throw new StorageException(ex);
}
}
public async Task<WorkerDataModel?> GetElementByIdAsync(string id, CancellationToken ct)
{
try
{
var worker = await _dbContext.Workers
.FirstOrDefaultAsync(x => x.Id == id, ct);
return _mapper.Map<WorkerDataModel>(worker);
}
catch (Exception ex)
{
_dbContext.ChangeTracker.Clear();
throw new StorageException(ex);
}
}
public WorkerDataModel? GetElementByFIO(string FIO)
{

View File

@@ -13,6 +13,8 @@ using TwoFromTheCasketContracts.StorageContracts;
using TwoFromTheCasketContracts.Enums;
using TwoFromTheCasketBusinessLogic.OfficePackage;
using TwoFromTheCasketContracts.Infastructure.SalaryConfiguration;
using TwoFromTheCasketContracts.BusinessLogicsContracts;
using TwoFromTheCasketContracts.ViewModels;
namespace TwoFromTheCasketTests.BusinessLogicsContractsTests;
@@ -24,6 +26,7 @@ public class ReportContractTests
private Mock<IRoomHistoryStorageContract> _roomHistoryStorageMock;
private Mock<IComplitedWorkStorageContract> _complitedWorkStorageMock;
private Mock<IWorkStorageContract> _workStorageMock;
private Mock<ISalaryStorageContract> _salaryStorageMock;
private Mock<IWorkerStorageContract> _workerStorageMock;
private Mock<BaseWordBuilder> _baseWordBuilderMock;
private Mock<BaseExcelBuilder> _baseExcelBuilderMock;
@@ -36,6 +39,7 @@ public class ReportContractTests
_roomHistoryStorageMock = new Mock<IRoomHistoryStorageContract>();
_complitedWorkStorageMock = new Mock<IComplitedWorkStorageContract>();
_workStorageMock = new Mock<IWorkStorageContract>();
_salaryStorageMock = new Mock<ISalaryStorageContract>();
_workerStorageMock = new Mock<IWorkerStorageContract>();
_baseWordBuilderMock = new Mock<BaseWordBuilder>();
_baseExcelBuilderMock = new Mock<BaseExcelBuilder>();
@@ -47,6 +51,7 @@ public class ReportContractTests
_complitedWorkStorageMock.Object,
_workStorageMock.Object,
_workerStorageMock.Object,
_salaryStorageMock.Object,
new Mock<ILogger>().Object,
_baseWordBuilderMock.Object,
_baseExcelBuilderMock.Object,
@@ -327,4 +332,84 @@ public class ReportContractTests
Assert.That(actualTableData[1][3], Is.EqualTo("Штукатурка"));
Assert.That(actualTableData[1][4], Is.EqualTo("1200,00"));
}
[Test]
public async Task GetWorkerSalariesByPeriodAsync_ReturnsCorrectData()
{
// Arrange
var workerId = Guid.NewGuid().ToString();
var from = new DateTime(2025, 4, 1);
var to = new DateTime(2025, 4, 30);
var salaries = new List<SalaryDataModel>
{
new(Guid.NewGuid().ToString(), workerId, 1200),
new(Guid.NewGuid().ToString(), workerId, 1300)
};
var worker = new WorkerDataModel(workerId, "Иванов Иван", Guid.NewGuid().ToString(), "+79031234567",
new DateTime(1990, 1, 1), new SalaryConfiguration { Rate = 100 });
_salaryStorageMock.Setup(x => x.GetListAsync(from, to, It.IsAny<CancellationToken>(), workerId)).ReturnsAsync(salaries);
_workerStorageMock.Setup(x => x.GetElementByIdAsync(workerId, It.IsAny<CancellationToken>())).ReturnsAsync(worker);
// Act
var result = await _reportContract.GetWorkerSalariesByPeriodAsync(workerId, from, to, CancellationToken.None);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(result.Count, Is.EqualTo(2));
Assert.That(result[0].WorkerFIO, Is.EqualTo("Иванов Иван"));
Assert.That(result[0].Amount, Is.EqualTo(1200));
Assert.That(result[1].Amount, Is.EqualTo(1300));
}
[Test]
public async Task CreateDocumentSalaryByWorkerAsync_ShouldCreateCorrectPieChart_Test()
{
// Arrange
var workerId = Guid.NewGuid().ToString();
var startDate = DateTime.UtcNow.AddDays(-10);
var endDate = DateTime.UtcNow;
var fio = "Иванов Иван";
var salaryList = new List<SalaryDataModel>
{
new(Guid.NewGuid().ToString(), workerId, 100),
new(Guid.NewGuid().ToString(), workerId, 200),
};
var worker = new WorkerDataModel(workerId, fio, Guid.NewGuid().ToString(), "+79000000000", DateTime.UtcNow.AddYears(-20), new SalaryConfiguration { Rate = 10 });
_salaryStorageMock.Setup(x => x.GetListAsync(startDate, endDate, It.IsAny<CancellationToken>(), workerId))
.ReturnsAsync(salaryList);
_workerStorageMock.Setup(x => x.GetElementByIdAsync(workerId, It.IsAny<CancellationToken>()))
.ReturnsAsync(worker);
List<(string, double)> pieData = null!;
_basePdfBuilderMock.Setup(x => x.AddHeader(It.IsAny<string>())).Returns(_basePdfBuilderMock.Object);
_basePdfBuilderMock.Setup(x => x.AddParagraph(It.IsAny<string>())).Returns(_basePdfBuilderMock.Object);
_basePdfBuilderMock.Setup(x => x.AddPieChart(It.IsAny<string>(), It.IsAny<List<(string, double)>>()))
.Callback((string title, List<(string, double)> data) => pieData = data)
.Returns(_basePdfBuilderMock.Object);
_basePdfBuilderMock.Setup(x => x.Build()).Returns(new MemoryStream());
// Act
var result = await _reportContract.CreateDocumentSalaryByWorkerAsync(workerId, startDate, endDate, CancellationToken.None);
// Assert
Assert.That(result, Is.Not.Null);
Assert.That(pieData, Is.Not.Null);
Assert.That(pieData.Count, Is.EqualTo(2));
Assert.That(pieData[0], Is.EqualTo((salaryList[0].Date.ToShortDateString(), 100)));
Assert.That(pieData[1], Is.EqualTo((salaryList[1].Date.ToShortDateString(), 200)));
_basePdfBuilderMock.Verify(x => x.AddHeader("Зарплатная ведомость"), Times.Once);
_basePdfBuilderMock.Verify(x => x.AddParagraph(It.Is<string>(s => s.Contains("за период"))), Times.Once);
_basePdfBuilderMock.Verify(x => x.AddPieChart("Начисления", It.IsAny<List<(string, double)>>()), Times.Once);
_basePdfBuilderMock.Verify(x => x.Build(), Times.Once);
}
}

View File

@@ -129,7 +129,49 @@ public class ReportControllerTests : BaseWebApiControllerTest
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
}
[Test]
public async Task GetWorkerSalaries_WhenHaveRecords_ShouldSuccess_Test()
{
var spec = TwoFromTheCasketDb.InsertSpecializationToDatabaseAndReturn();
var worker = TwoFromTheCasketDb.InsertWorkerToDatabaseAndReturn(specializationId: spec.Id, fio: "Иванов Иван");
var salary1 = TwoFromTheCasketDb.InsertSalaryToDatabaseAndReturn(workerId: worker.Id, sum: 1000, date: DateTime.UtcNow.AddDays(-10));
var salary2 = TwoFromTheCasketDb.InsertSalaryToDatabaseAndReturn(workerId: worker.Id, sum: 1500, date: DateTime.UtcNow.AddDays(-5));
var fromDate = DateTime.UtcNow.AddDays(-15).ToString("O");
var toDate = DateTime.UtcNow.ToString("O");
var response = await HttpClient.GetAsync($"/api/report/GetWorkerSalaries?workerId={worker.Id}&fromDate={fromDate}&toDate={toDate}");
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var data = await GetModelFromResponseAsync<List<SalaryByWorkerReportViewModel>>(response);
Assert.That(data, Is.Not.Null);
Assert.That(data, Has.Count.EqualTo(2));
Assert.That(data[0].WorkerFIO, Is.EqualTo(worker.FIO));
Assert.That(data[1].Amount, Is.EqualTo(1500).Or.EqualTo(1000));
}
[Test]
public async Task LoadWorkerSalaries_WhenHaveRecords_ShouldSuccess_Test()
{
// Arrange
var spec = TwoFromTheCasketDb.InsertSpecializationToDatabaseAndReturn();
var worker = TwoFromTheCasketDb.InsertWorkerToDatabaseAndReturn(specializationId: spec.Id, fio: "Иванов Иван");
TwoFromTheCasketDb.InsertSalaryToDatabaseAndReturn(workerId: worker.Id, sum: 500);
TwoFromTheCasketDb.InsertSalaryToDatabaseAndReturn(workerId: worker.Id, sum: 1000);
var from = DateTime.UtcNow.AddDays(-10).ToString("yyyy-MM-dd");
var to = DateTime.UtcNow.AddDays(10).ToString("yyyy-MM-dd");
// Act
var response = await HttpClient.GetAsync($"/api/report/LoadWorkerSalaries?workerId={worker.Id}&fromDate={from}&toDate={to}");
// Assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
Assert.That(response.Content.Headers.ContentType?.MediaType, Is.EqualTo("application/octet-stream"));
Assert.That(response.Content.Headers.ContentDisposition?.FileName, Is.EqualTo("salary.pdf"));
Assert.That(await response.Content.ReadAsStreamAsync(), Is.Not.Null);
}
private static async Task<T> GetModelFromResponseAsync<T>(HttpResponseMessage response)
{

View File

@@ -79,6 +79,46 @@ public class ReportAdapter : IReportAdapter
}
}
public async Task<ReportOperationResponse> CreateDocumentSalaryByWorkerAsync(string workerId, DateTime from, DateTime to, CancellationToken ct)
{
try
{
var stream = await _reportLogic.CreateDocumentSalaryByWorkerAsync(workerId, from, to, ct);
return ReportOperationResponse.OK(stream, "salary.pdf");
}
catch (ArgumentNullException ex)
{
_logger.LogError(ex, "ArgumentNullException");
return ReportOperationResponse.BadRequest("Worker ID cannot be null or empty.");
}
catch (ValidationException ex)
{
_logger.LogError(ex, "ValidationException");
return ReportOperationResponse.BadRequest("Invalid worker ID format.");
}
catch (IncorrectDatesException ex)
{
_logger.LogError(ex, "IncorrectDatesException");
return ReportOperationResponse.BadRequest($"Invalid dates: {ex.Message}");
}
catch (InvalidOperationException ex)
{
_logger.LogError(ex, "InvalidOperationException");
return ReportOperationResponse.BadRequest("Salary data not found.");
}
catch (StorageException ex)
{
_logger.LogError(ex, "StorageException");
return ReportOperationResponse.InternalServerError($"Storage error: {ex.InnerException?.Message ?? ex.Message}");
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception");
return ReportOperationResponse.InternalServerError(ex.Message);
}
}
public async Task<ReportOperationResponse> GetComplitedWorksByPeriodAsync(DateTime from, DateTime to, CancellationToken ct)
{
try
@@ -127,4 +167,35 @@ public class ReportAdapter : IReportAdapter
return ReportOperationResponse.InternalServerError(ex.Message);
}
}
public async Task<ReportOperationResponse> GetWorkerSalariesByPeriodAsync(string workerId, DateTime from, DateTime to, CancellationToken ct)
{
try
{
var result = await _reportLogic.GetWorkerSalariesByPeriodAsync(workerId, from, to, ct);
var mapped = result.Select(x => _mapper.Map<SalaryByWorkerReportViewModel>(x)).ToList();
return ReportOperationResponse.OK(mapped);
}
catch (IncorrectDatesException ex)
{
_logger.LogError(ex, "IncorrectDatesException");
return ReportOperationResponse.BadRequest("The end date must be later than the start date.");
}
catch (ArgumentNullException ex)
{
_logger.LogError(ex, "ArgumentNullException");
return ReportOperationResponse.BadRequest("Worker ID must be provided.");
}
catch (StorageException ex)
{
_logger.LogError(ex, "StorageException");
return ReportOperationResponse.InternalServerError($"Storage error: {ex.InnerException?.Message ?? ex.Message}");
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception");
return ReportOperationResponse.InternalServerError(ex.Message);
}
}
}

View File

@@ -25,17 +25,32 @@ public class ReportController(IReportAdapter reportAdapter) : ControllerBase
{
return (await _reportAdapter.GetComplitedWorksByPeriodAsync(fromDate, toDate, ct)).GetResponse(Request, Response);
}
[HttpGet]
[Consumes("application/json")]
public async Task<IActionResult> GetWorkerSalaries(string workerId, DateTime fromDate, DateTime toDate, CancellationToken ct)
{
return (await _reportAdapter.GetWorkerSalariesByPeriodAsync(workerId, fromDate, toDate, ct)).GetResponse(Request, Response);
}
[HttpGet]
[Consumes("application/octet-stream")]
public async Task<IActionResult> LoadRoomHistory(CancellationToken ct)
{
return (await _reportAdapter.CreateDocumentRoomHistoryAsync(ct)).GetResponse(Request, Response);
}
[HttpGet]
[Consumes("application/octet-stream")]
public async Task<IActionResult> LoadComplitedWork(DateTime fromDate, DateTime toDate, CancellationToken ct)
{
return (await _reportAdapter.CreateDocumentComplitedWorksAsync(fromDate, toDate, ct)).GetResponse(Request, Response);
}
[HttpGet]
[Consumes("application/octet-stream")]
public async Task<IActionResult> LoadWorkerSalaries(string workerId, DateTime fromDate, DateTime toDate, CancellationToken ct)
{
return (await _reportAdapter.CreateDocumentSalaryByWorkerAsync(workerId, fromDate, toDate, ct)).GetResponse(Request, Response);
}
}