5 Commits

Author SHA1 Message Date
maxim
9699999f8a fix problem 2025-09-14 08:44:15 +04:00
maxim
1b6691fa23 тест сломался 2025-09-12 14:52:25 +04:00
maxim
95e254c5c1 бб 2025-09-12 14:18:44 +04:00
maxim
8c31a8d9e2 б 2025-09-12 14:16:41 +04:00
nezui1
beb2573977 соответствуют требованиям 2025-09-12 11:38:17 +04:00
24 changed files with 1096 additions and 178 deletions

View File

@@ -39,32 +39,61 @@ internal class ReportContract(IServiceStorageContract serviceStorageContract, IM
public async Task<Stream> CreateDocumentServicesWithHistoryAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct)
{
var masters = await GetDataMastersWithHistoryAsync(dateStart, dateFinish, ct);
var services = await GetDataServicesWithHistoryAsync(dateStart, dateFinish, ct);
var builder = _baseWordBuilder
.AddHeader("Отчет по мастерам за период")
.AddHeader("Отчет по услугам и их истории изменений")
.AddParagraph($"Период: с {dateStart:dd.MM.yyyy} по {dateFinish:dd.MM.yyyy}")
.AddParagraph($"Отчёт сформирован: {DateTime.Now:dd.MM.yyyy HH:mm}");
if (!masters.Any())
if (!services.Any())
{
builder.AddParagraph("За указанный период мастера не найдены.");
builder.AddParagraph("За указанный период услуги не найдены.");
return builder.Build();
}
var tableData = new List<string[]> { new[] { "ФИО мастера", "Дата трудоустройства", "Статус", "Количество заказов" } };
foreach (var master in masters)
var tableData = new List<string[]>
{
tableData.Add(new[]
new[] { "Услуга", "Тип", "Текущая цена", "Дата изменения", "Старая цена" }
};
foreach (var service in services)
{
if (service.History.Any())
{
master.FIO,
master.EmploymentDate.ToString("dd.MM.yyyy"),
master.IsDeleted ? "Уволен" : "Работает",
master.OrdersCount.ToString()
});
var sortedHistory = service.History.OrderByDescending(h => h.ChangeDate).ToList();
for (int i = 0; i < sortedHistory.Count; i++)
{
var history = sortedHistory[i];
tableData.Add(new[]
{
i == 0 ? service.ServiceName : "",
i == 0 ? service.ServiceType.ToString() : "",
i == 0 ? service.Price.ToString("C") : "",
history.ChangeDate.ToString("dd.MM.yyyy HH:mm"),
history.OldPrice.ToString("C")
});
}
}
else
{
tableData.Add(new[]
{
service.ServiceName,
service.ServiceType.ToString(),
service.Price.ToString("C"),
"—",
"—"
});
}
}
builder.AddTable(new[] { 3000, 2000, 1500, 2000 }, tableData);
builder.AddTable(new[] { 2500, 1500, 1500, 2000, 1500 }, tableData);
return builder.Build();
}
@@ -72,33 +101,93 @@ internal class ReportContract(IServiceStorageContract serviceStorageContract, IM
{
var orders = await GetDataOrderByPeriodAsync(dateStart, dateFinish, ct);
var tableData = new List<string[]> { new[] { "Дата заказа", "Статус", "Тип помещения", "ID заказа" } };
var tableData = new List<string[]> { new[] { "Дата заказа", "Статус", "Тип помещения", "Мастер", "Услуга", "Время работы", "Сумма" } };
foreach (var order in orders)
{
tableData.Add(new[]
var orderDetails = await GetOrderDetailsAsync(order.Id, ct);
if (orderDetails.Any())
{
order.Date.ToString("dd.MM.yyyy"),
order.Status.ToString(),
order.RoomType.ToString(),
order.Id
});
foreach (var detail in orderDetails)
{
tableData.Add(new[]
{
order.Date.ToString("dd.MM.yyyy"),
order.Status.ToString(),
order.RoomType.ToString(),
detail.MasterFIO,
detail.ServiceName,
$"{detail.TimeOfWorking} ч.",
detail.TotalAmount.ToString("C")
});
}
}
else
{
tableData.Add(new[]
{
order.Date.ToString("dd.MM.yyyy"),
order.Status.ToString(),
order.RoomType.ToString(),
"Не назначен",
"Нет услуг",
"0 ч.",
"0,00 ₽"
});
}
}
return _baseExcelBuilder
.AddHeader("Заказы за период", 0, 4)
.AddHeader("Отчет по продажам за период", 0, 7)
.AddParagraph($"с {dateStart.ToShortDateString()} по {dateFinish.ToShortDateString()}", 2)
.AddTable([20, 15, 20, 30], tableData)
.AddTable([15, 12, 15, 20, 25, 12, 15], tableData)
.Build();
}
public async Task<Stream> CreateDocumentSalaryByPeriodAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct)
{
var data = await GetDataSalaryByPeriodAsync(dateStart, dateFinish, ct);
if (!data.Any())
{
return _basePdfBuilder
.AddHeader("Зарплатная ведомость")
.AddParagraph($"за период с {dateStart.ToShortDateString()} по {dateFinish.ToShortDateString()}")
.AddParagraph("За указанный период зарплаты не найдены.")
.Build();
}
var groupedData = data.GroupBy(x => x.MasterFIO)
.Select(g => new { MasterFIO = g.Key, TotalSalary = g.Sum(x => x.TotalSalary) })
.ToList();
var tableData = new List<string[]>
{
new[] { "ФИО мастера", "Дата зачисления", "Сумма зарплаты" }
};
foreach (var salary in data)
{
var row = new[]
{
salary.MasterFIO,
salary.FromPeriod.ToString("dd.MM.yyyy"),
salary.TotalSalary.ToString("C")
};
tableData.Add(row);
}
return _basePdfBuilder
.AddHeader("Зарплатная ведомость")
.AddParagraph($"за период с {dateStart.ToShortDateString()} по {dateFinish.ToShortDateString()}")
.AddPieChart("Начисления", [.. data.Select(x => (x.MasterFIO, x.TotalSalary))])
.AddPieChart("Начисления", groupedData.Select(x => (x.MasterFIO, x.TotalSalary)).ToList())
.AddParagraph("")
.AddTable(new[] { 200, 150, 150 }, tableData)
.Build();
}
@@ -128,7 +217,7 @@ internal class ReportContract(IServiceStorageContract serviceStorageContract, IM
try
{
// Получаем все заказы за указанный период
var orders = await _orderStorageContract.GetListAsync(dateStart, dateFinish, ct);
return orders.OrderBy(o => o.Date).ToList();
}
@@ -140,7 +229,7 @@ internal class ReportContract(IServiceStorageContract serviceStorageContract, IM
public async Task<List<MasterSalaryByPeriodDataModel>> GetDataSalaryByPeriodAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct)
{
if (dateStart.IsDateNotOlder(dateFinish))
if (dateStart > dateFinish)
{
throw new IncorrectDatesException(dateStart, dateFinish);
}
@@ -148,54 +237,75 @@ internal class ReportContract(IServiceStorageContract serviceStorageContract, IM
var salaries = await _salaryStorageContract.GetListAsync(dateStart, dateFinish, ct);
var masters = _masterStorageContract.GetList(onlyActive: true) ?? new List<MasterDataModel>();
var groupedSalaries = salaries.GroupBy(s => s.MasterId).Select(group =>
{
var master = masters.FirstOrDefault(m => m.Id == group.Key);
return new MasterSalaryByPeriodDataModel
var salaryRecords = salaries
.GroupBy(salary => salary.MasterId)
.Select(group =>
{
MasterFIO = master?.FIO ?? "Неизвестный мастер",
TotalSalary = group.Sum(s => s.Salary + s.Prize),
FromPeriod = group.Min(s => s.SalaryDate),
ToPeriod = group.Max(s => s.SalaryDate)
};
}).OrderBy(x => x.MasterFIO).ToList();
var master = masters.FirstOrDefault(m => m.Id == group.Key);
var totalSalary = group.Sum(salary => salary.Salary + salary.Prize);
return groupedSalaries;
return new MasterSalaryByPeriodDataModel
{
MasterFIO = master?.FIO ?? "Неизвестный мастер",
TotalSalary = totalSalary,
FromPeriod = dateStart,
ToPeriod = dateFinish
};
})
.OrderBy(x => x.MasterFIO)
.ToList();
return salaryRecords;
}
public async Task<List<MasterWithHistoryDataModel>> GetDataMastersWithHistoryAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct)
public async Task<List<ServiceWithHistoryDataModel>> GetDataServicesWithHistoryAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct)
{
if (dateStart.IsDateNotOlder(dateFinish))
{
throw new IncorrectDatesException(dateStart, dateFinish);
}
// Получаем всех мастеров (включая уволенных)
var masters = _masterStorageContract.GetList(onlyActive: false) ?? new List<MasterDataModel>();
// Получаем заказы за указанный период
var orders = await _orderStorageContract.GetListAsync(dateStart, dateFinish, ct);
var services = _serviceStorageContract.GetList() ?? new List<ServiceDataModel>();
// Получаем связи заказов с мастерами через ServiceOrder
// Пока что просто считаем, что у каждого мастера 0 заказов, так как у нас нет доступа к ServiceOrder
// В реальном проекте нужно было бы добавить метод для получения ServiceOrder
var result = new List<ServiceWithHistoryDataModel>();
var result = masters.Select(master =>
foreach (var service in services)
{
// Временно ставим 0, так как нет прямого доступа к ServiceOrder
var ordersCount = 0;
return new MasterWithHistoryDataModel
var serviceHistories = _serviceStorageContract.GetHistoryByServiceId(service.Id) ?? new List<ServiceHistoryDataModel>();
var filteredHistories = serviceHistories
.Where(h => h.ChangeDate >= dateStart && h.ChangeDate <= dateFinish)
.OrderByDescending(h => h.ChangeDate)
.ToList();
result.Add(new ServiceWithHistoryDataModel
{
Id = master.Id,
FIO = master.FIO,
EmploymentDate = master.EmploymentDate,
IsDeleted = master.IsDeleted,
OrdersCount = ordersCount
};
}).ToList();
Id = service.Id,
ServiceName = service.ServiceName,
ServiceType = service.ServiceType,
MasterId = service.MasterId,
Price = service.Price,
IsDeleted = service.IsDeleted,
History = filteredHistories
});
}
return result;
return result.OrderBy(s => s.ServiceName).ToList();
}
private async Task<List<(string MasterFIO, string ServiceName, int TimeOfWorking, decimal TotalAmount)>> GetOrderDetailsAsync(string orderId, CancellationToken ct)
{
try
{
return await _orderStorageContract.GetOrderDetailsAsync(orderId, ct);
}
catch (Exception)
{
return new List<(string MasterFIO, string ServiceName, int TimeOfWorking, decimal TotalAmount)>();
}
}
}

View File

@@ -11,5 +11,6 @@ public abstract class BasePdfBuilder
public abstract BasePdfBuilder AddHeader(string header);
public abstract BasePdfBuilder AddParagraph(string text);
public abstract BasePdfBuilder AddPieChart(string title, List<(string Caption, double Value)> data);
public abstract BasePdfBuilder AddTable(int[] widths, List<string[]> data);
public abstract Stream Build();
}

View File

@@ -54,6 +54,48 @@ internal class MigraDocPdfBuilder : BasePdfBuilder
_document.LastSection.Add(chart);
return this;
}
public override BasePdfBuilder AddTable(int[] widths, List<string[]> data)
{
if (data == null || data.Count == 0)
{
return this;
}
var table = _document.LastSection.AddTable();
table.Style = "Table";
table.Borders.Color = Colors.Black;
table.Borders.Width = 0.25;
table.Borders.Left.Width = 0.5;
table.Borders.Right.Width = 0.5;
table.Rows.LeftIndent = 0;
// Настройка колонок
for (int i = 0; i < widths.Length; i++)
{
var column = table.AddColumn(Unit.FromPoint(widths[i]));
column.Format.Alignment = ParagraphAlignment.Center;
}
// Добавление данных
for (int rowIndex = 0; rowIndex < data.Count; rowIndex++)
{
var row = table.AddRow();
row.HeadingFormat = (rowIndex == 0); // Первая строка - заголовок
row.Format.Alignment = ParagraphAlignment.Center;
row.Format.Font.Bold = (rowIndex == 0);
for (int colIndex = 0; colIndex < data[rowIndex].Length && colIndex < widths.Length; colIndex++)
{
var cell = row.Cells[colIndex];
var cellText = data[rowIndex][colIndex];
cell.AddParagraph(cellText);
cell.Format.Alignment = ParagraphAlignment.Center;
}
}
return this;
}
public override Stream Build()
{
var stream = new MemoryStream();

View File

@@ -9,13 +9,13 @@ namespace TwoFromTheCasketContratcs.AdapterContracts;
public interface IReportAdapter
{
Task<ReportOperationResponse> GetDataMastersWithHistoryAsync(CancellationToken ct);
Task<ReportOperationResponse> GetDataServicesWithHistoryAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct);
Task<ReportOperationResponse> GetDataOrderByPeriodAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct);
Task<ReportOperationResponse> GetDataSalaryByPeriodAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct);
Task<ReportOperationResponse> CreateDocumentMastersWithHistoryAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct);
Task<ReportOperationResponse> CreateDocumentServicesWithHistoryAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct);
Task<ReportOperationResponse> CreateDocumentOrdersByPeriodAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct);

View File

@@ -16,6 +16,8 @@ public class ReportOperationResponse : OperationResponse
public static ReportOperationResponse OK(List<MasterSalaryByPeriodViewModel> data) => OK<ReportOperationResponse, List<MasterSalaryByPeriodViewModel>>(data);
public static ReportOperationResponse OK(List<ServiceWithHistoryViewModel> data) => OK<ReportOperationResponse, List<ServiceWithHistoryViewModel>>(data);
public static ReportOperationResponse OK(Stream data, string fileName) => OK<ReportOperationResponse, Stream>(data, fileName);
public static ReportOperationResponse BadRequest(string message) =>

View File

@@ -15,7 +15,7 @@ public interface IReportContract
Task<List<MasterSalaryByPeriodDataModel>> GetDataSalaryByPeriodAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct);
Task<List<MasterWithHistoryDataModel>> GetDataMastersWithHistoryAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct);
Task<List<ServiceWithHistoryDataModel>> GetDataServicesWithHistoryAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct);
Task<Stream> CreateDocumentServicesWithHistoryAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct);

View File

@@ -1,10 +0,0 @@
namespace TwoFromTheCasketContratcs.DataModels;
public class MasterWithHistoryDataModel
{
public string Id { get; set; } = string.Empty;
public string FIO { get; set; } = string.Empty;
public DateTime EmploymentDate { get; set; }
public bool IsDeleted { get; set; }
public int OrdersCount { get; set; }
}

View File

@@ -0,0 +1,15 @@
using TwoFromTheCasketContratcs.Enums;
namespace TwoFromTheCasketContratcs.DataModels;
public class ServiceWithHistoryDataModel
{
public string Id { get; set; } = string.Empty;
public string ServiceName { get; set; } = string.Empty;
public ServiceType ServiceType { get; set; }
public double Price { get; set; }
public string MasterId { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public bool IsDeleted { get; set; }
public List<ServiceHistoryDataModel> History { get; set; } = new();
}

View File

@@ -25,4 +25,6 @@ public interface IOrderStorageContract
void UpdElement(OrderDataModel orderDataModel);
void DelElement(string id);
Task<List<(string MasterFIO, string ServiceName, int TimeOfWorking, decimal TotalAmount)>> GetOrderDetailsAsync(string orderId, CancellationToken ct);
}

View File

@@ -8,6 +8,8 @@ namespace TwoFromTheCasketContratcs.ViewModels;
public class ServiceHistoryViewModel
{
public int Id { get; set; }
public string ServiceId { get; set; } = string.Empty;
public double OldPrice { get; set; }

View File

@@ -0,0 +1,15 @@
using TwoFromTheCasketContratcs.Enums;
namespace TwoFromTheCasketContratcs.ViewModels;
public class ServiceWithHistoryViewModel
{
public string Id { get; set; } = string.Empty;
public string ServiceName { get; set; } = string.Empty;
public ServiceType ServiceType { get; set; }
public double Price { get; set; }
public string MasterId { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
public bool IsDeleted { get; set; }
public List<ServiceHistoryViewModel> History { get; set; } = new();
}

View File

@@ -154,6 +154,36 @@ internal class OrderStorageContract : IOrderStorageContract
private Order? GetOrderById(string id) => _dbContext.Orders.FirstOrDefault(x => x.Id == id);
public async Task<List<(string MasterFIO, string ServiceName, int TimeOfWorking, decimal TotalAmount)>> GetOrderDetailsAsync(string orderId, CancellationToken ct)
{
try
{
var orderDetails = await _dbContext.ServiceOrders
.Where(so => so.OrderId == orderId)
.Join(_dbContext.Masters, so => so.MasterId, m => m.Id, (so, m) => new { so, m })
.Join(_dbContext.Services, x => x.so.ServiceId, s => s.Id, (x, s) => new
{
MasterFIO = x.m.FIO,
ServiceName = s.ServiceName,
TimeOfWorking = x.so.TimeOfWorking,
ServicePrice = s.Price
})
.ToListAsync(ct);
return orderDetails.Select(x => (
x.MasterFIO,
x.ServiceName,
x.TimeOfWorking,
(decimal)(x.ServicePrice * x.TimeOfWorking) // Простой расчет: цена услуги * время работы
)).ToList();
}
catch (Exception ex)
{
_dbContext.ChangeTracker.Clear();
throw new StorageException(ex);
}
}
public async Task<List<OrderDataModel>> GetListAsync(DateTime startDate, DateTime endDate, CancellationToken ct)
{
try

View File

@@ -0,0 +1,313 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using TwoFromTheCasketDatabase;
#nullable disable
namespace TwoFromTheCasketDatabase.Migrations
{
[DbContext(typeof(TwoFromTheCasketDbContext))]
[Migration("20250912071637_FixServiceOrderStructure")]
partial class FixServiceOrderStructure
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("TwoFromTheCasketDatabase.Models.Master", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<DateTime>("BirthDate")
.HasColumnType("timestamp with time zone");
b.Property<DateTime>("EmploymentDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("FIO")
.IsRequired()
.HasColumnType("text");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<string>("PostId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("Id", "IsDeleted")
.IsUnique()
.HasFilter("\"IsDeleted\" = FALSE");
b.ToTable("Masters");
});
modelBuilder.Entity("TwoFromTheCasketDatabase.Models.Order", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<DateTime>("Date")
.HasColumnType("timestamp with time zone");
b.Property<int>("RoomType")
.HasColumnType("integer");
b.Property<int>("Status")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("Id")
.IsUnique();
b.ToTable("Orders");
});
modelBuilder.Entity("TwoFromTheCasketDatabase.Models.Post", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<DateTime>("ChangeDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Configuration")
.IsRequired()
.HasColumnType("jsonb");
b.Property<bool>("IsActual")
.HasColumnType("boolean");
b.Property<string>("PostId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("PostName")
.IsRequired()
.HasColumnType("text");
b.Property<int>("PostType")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("PostId", "IsActual")
.IsUnique()
.HasFilter("\"IsActual\" = TRUE");
b.HasIndex("PostName", "IsActual")
.IsUnique()
.HasFilter("\"IsActual\" = TRUE");
b.ToTable("Posts");
});
modelBuilder.Entity("TwoFromTheCasketDatabase.Models.Salary", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<string>("MasterId")
.IsRequired()
.HasColumnType("text");
b.Property<double>("Prize")
.HasColumnType("double precision");
b.Property<DateTime>("SalaryDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("SalaryId")
.HasColumnType("text");
b.Property<double>("SalarySize")
.HasColumnType("double precision");
b.HasKey("Id");
b.HasIndex("MasterId", "SalaryDate")
.IsUnique();
b.ToTable("Salaries");
});
modelBuilder.Entity("TwoFromTheCasketDatabase.Models.Service", b =>
{
b.Property<string>("Id")
.HasColumnType("text");
b.Property<bool>("IsDeleted")
.HasColumnType("boolean");
b.Property<string>("MasterId")
.IsRequired()
.HasColumnType("text");
b.Property<double>("Price")
.HasColumnType("double precision");
b.Property<string>("ServiceId")
.HasColumnType("text");
b.Property<string>("ServiceName")
.IsRequired()
.HasColumnType("text");
b.Property<int>("ServiceType")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("ServiceId");
b.HasIndex("ServiceName", "IsDeleted")
.IsUnique()
.HasFilter("\"IsDeleted\" = FALSE");
b.ToTable("Services");
});
modelBuilder.Entity("TwoFromTheCasketDatabase.Models.ServiceHistory", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("ChangeDate")
.HasColumnType("timestamp with time zone");
b.Property<double>("OldPrice")
.HasColumnType("double precision");
b.Property<string>("ServiceId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ServiceId");
b.ToTable("ServiceHistories");
});
modelBuilder.Entity("TwoFromTheCasketDatabase.Models.ServiceOrder", b =>
{
b.Property<string>("ServiceOrderId")
.HasColumnType("text");
b.Property<string>("MasterId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("OrderId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ServiceId")
.IsRequired()
.HasColumnType("text");
b.Property<int>("TimeOfWorking")
.HasColumnType("integer");
b.HasKey("ServiceOrderId");
b.HasIndex("MasterId");
b.HasIndex("OrderId");
b.HasIndex("ServiceId");
b.ToTable("ServiceOrders");
});
modelBuilder.Entity("TwoFromTheCasketDatabase.Models.Salary", b =>
{
b.HasOne("TwoFromTheCasketDatabase.Models.Master", "Master")
.WithMany("Salaries")
.HasForeignKey("MasterId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Master");
});
modelBuilder.Entity("TwoFromTheCasketDatabase.Models.Service", b =>
{
b.HasOne("TwoFromTheCasketDatabase.Models.Master", null)
.WithMany("Services")
.HasForeignKey("ServiceId");
});
modelBuilder.Entity("TwoFromTheCasketDatabase.Models.ServiceHistory", b =>
{
b.HasOne("TwoFromTheCasketDatabase.Models.Service", "Service")
.WithMany("ServiceHistory")
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Service");
});
modelBuilder.Entity("TwoFromTheCasketDatabase.Models.ServiceOrder", b =>
{
b.HasOne("TwoFromTheCasketDatabase.Models.Master", null)
.WithMany()
.HasForeignKey("MasterId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("TwoFromTheCasketDatabase.Models.Order", null)
.WithMany()
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("TwoFromTheCasketDatabase.Models.Service", null)
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("TwoFromTheCasketDatabase.Models.Master", null)
.WithMany("ServiceOrders")
.HasForeignKey("ServiceOrderId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("TwoFromTheCasketDatabase.Models.Master", b =>
{
b.Navigation("Salaries");
b.Navigation("ServiceOrders");
b.Navigation("Services");
});
modelBuilder.Entity("TwoFromTheCasketDatabase.Models.Service", b =>
{
b.Navigation("ServiceHistory");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,148 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace TwoFromTheCasketDatabase.Migrations
{
/// <inheritdoc />
public partial class FixServiceOrderStructure : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropPrimaryKey(
name: "PK_ServiceOrders",
table: "ServiceOrders");
migrationBuilder.DropIndex(
name: "IX_ServiceOrders_ServiceOrderId",
table: "ServiceOrders");
migrationBuilder.DropPrimaryKey(
name: "PK_ServiceHistories",
table: "ServiceHistories");
migrationBuilder.AddColumn<int>(
name: "Id",
table: "ServiceHistories",
type: "integer",
nullable: false,
defaultValue: 0)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
migrationBuilder.AddPrimaryKey(
name: "PK_ServiceOrders",
table: "ServiceOrders",
column: "ServiceOrderId");
migrationBuilder.AddPrimaryKey(
name: "PK_ServiceHistories",
table: "ServiceHistories",
column: "Id");
migrationBuilder.CreateIndex(
name: "IX_ServiceOrders_MasterId",
table: "ServiceOrders",
column: "MasterId");
migrationBuilder.CreateIndex(
name: "IX_ServiceOrders_OrderId",
table: "ServiceOrders",
column: "OrderId");
migrationBuilder.CreateIndex(
name: "IX_ServiceOrders_ServiceId",
table: "ServiceOrders",
column: "ServiceId");
migrationBuilder.CreateIndex(
name: "IX_ServiceHistories_ServiceId",
table: "ServiceHistories",
column: "ServiceId");
migrationBuilder.AddForeignKey(
name: "FK_ServiceOrders_Masters_MasterId",
table: "ServiceOrders",
column: "MasterId",
principalTable: "Masters",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_ServiceOrders_Orders_OrderId",
table: "ServiceOrders",
column: "OrderId",
principalTable: "Orders",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
migrationBuilder.AddForeignKey(
name: "FK_ServiceOrders_Services_ServiceId",
table: "ServiceOrders",
column: "ServiceId",
principalTable: "Services",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_ServiceOrders_Masters_MasterId",
table: "ServiceOrders");
migrationBuilder.DropForeignKey(
name: "FK_ServiceOrders_Orders_OrderId",
table: "ServiceOrders");
migrationBuilder.DropForeignKey(
name: "FK_ServiceOrders_Services_ServiceId",
table: "ServiceOrders");
migrationBuilder.DropPrimaryKey(
name: "PK_ServiceOrders",
table: "ServiceOrders");
migrationBuilder.DropIndex(
name: "IX_ServiceOrders_MasterId",
table: "ServiceOrders");
migrationBuilder.DropIndex(
name: "IX_ServiceOrders_OrderId",
table: "ServiceOrders");
migrationBuilder.DropIndex(
name: "IX_ServiceOrders_ServiceId",
table: "ServiceOrders");
migrationBuilder.DropPrimaryKey(
name: "PK_ServiceHistories",
table: "ServiceHistories");
migrationBuilder.DropIndex(
name: "IX_ServiceHistories_ServiceId",
table: "ServiceHistories");
migrationBuilder.DropColumn(
name: "Id",
table: "ServiceHistories");
migrationBuilder.AddPrimaryKey(
name: "PK_ServiceOrders",
table: "ServiceOrders",
columns: new[] { "ServiceId", "MasterId", "OrderId" });
migrationBuilder.AddPrimaryKey(
name: "PK_ServiceHistories",
table: "ServiceHistories",
column: "ServiceId");
migrationBuilder.CreateIndex(
name: "IX_ServiceOrders_ServiceOrderId",
table: "ServiceOrders",
column: "ServiceOrderId");
}
}
}

View File

@@ -181,8 +181,11 @@ namespace TwoFromTheCasketDatabase.Migrations
modelBuilder.Entity("TwoFromTheCasketDatabase.Models.ServiceHistory", b =>
{
b.Property<string>("ServiceId")
.HasColumnType("text");
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<DateTime>("ChangeDate")
.HasColumnType("timestamp with time zone");
@@ -190,32 +193,44 @@ namespace TwoFromTheCasketDatabase.Migrations
b.Property<double>("OldPrice")
.HasColumnType("double precision");
b.HasKey("ServiceId");
b.Property<string>("ServiceId")
.IsRequired()
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("ServiceId");
b.ToTable("ServiceHistories");
});
modelBuilder.Entity("TwoFromTheCasketDatabase.Models.ServiceOrder", b =>
{
b.Property<string>("ServiceId")
b.Property<string>("ServiceOrderId")
.HasColumnType("text");
b.Property<string>("MasterId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("OrderId")
.IsRequired()
.HasColumnType("text");
b.Property<string>("ServiceOrderId")
b.Property<string>("ServiceId")
.IsRequired()
.HasColumnType("text");
b.Property<int>("TimeOfWorking")
.HasColumnType("integer");
b.HasKey("ServiceId", "MasterId", "OrderId");
b.HasKey("ServiceOrderId");
b.HasIndex("ServiceOrderId");
b.HasIndex("MasterId");
b.HasIndex("OrderId");
b.HasIndex("ServiceId");
b.ToTable("ServiceOrders");
});
@@ -251,6 +266,24 @@ namespace TwoFromTheCasketDatabase.Migrations
modelBuilder.Entity("TwoFromTheCasketDatabase.Models.ServiceOrder", b =>
{
b.HasOne("TwoFromTheCasketDatabase.Models.Master", null)
.WithMany()
.HasForeignKey("MasterId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("TwoFromTheCasketDatabase.Models.Order", null)
.WithMany()
.HasForeignKey("OrderId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("TwoFromTheCasketDatabase.Models.Service", null)
.WithMany()
.HasForeignKey("ServiceId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("TwoFromTheCasketDatabase.Models.Master", null)
.WithMany("ServiceOrders")
.HasForeignKey("ServiceOrderId")

View File

@@ -30,7 +30,6 @@ internal class Master
[ForeignKey("SalaryId")]
public List<Salary>? Salaries { get; set; }
[ForeignKey("ServiceOrderId")]
public List<ServiceOrder>? ServiceOrders { get; set; }
}

View File

@@ -9,6 +9,8 @@ namespace TwoFromTheCasketDatabase.Models;
internal class ServiceHistory
{
public string Id { get; set; } = Guid.NewGuid().ToString();
public required string ServiceId { get; set; }
public required double OldPrice { get; set; }

View File

@@ -9,6 +9,7 @@ namespace TwoFromTheCasketDatabase.Models;
internal class ServiceOrder
{
public string ServiceOrderId { get; set; } = Guid.NewGuid().ToString();
public required string OrderId { get; set; }

View File

@@ -41,11 +41,30 @@ internal class TwoFromTheCasketDbContext(IConfigurationDatabase configurationDat
.IsUnique()
.HasFilter($"\"{nameof(Post.IsActual)}\" = TRUE");
modelBuilder.Entity<ServiceHistory>().HasKey(x => x.ServiceId);
modelBuilder.Entity<ServiceHistory>().HasKey(x => x.Id);
modelBuilder.Entity<Service>().HasIndex(e => new { e.ServiceName, e.IsDeleted }).IsUnique().HasFilter($"\"{nameof(Service.IsDeleted)}\" = FALSE");
modelBuilder.Entity<ServiceOrder>().HasKey(x => new { x.ServiceId, x.MasterId, x.OrderId });
modelBuilder.Entity<ServiceOrder>().HasKey(x => x.ServiceOrderId);
// Настройка внешних ключей для ServiceOrder
modelBuilder.Entity<ServiceOrder>()
.HasOne<Order>()
.WithMany()
.HasForeignKey(so => so.OrderId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<ServiceOrder>()
.HasOne<Service>()
.WithMany()
.HasForeignKey(so => so.ServiceId)
.OnDelete(DeleteBehavior.Restrict);
modelBuilder.Entity<ServiceOrder>()
.HasOne<Master>()
.WithMany(m => m.ServiceOrders)
.HasForeignKey(so => so.MasterId)
.OnDelete(DeleteBehavior.Restrict);
// Настройка внешнего ключа для Salary
modelBuilder.Entity<Salary>()

View File

@@ -165,7 +165,7 @@ internal class ReportContractTests
public async Task GetDataSalaryByPeriod_ShouldSuccess_Test()
{
//Arrange
var startDate = DateTime.UtcNow.AddDays(-1);
var startDate = DateTime.UtcNow.AddDays(-5);
var endDate = DateTime.UtcNow;
var master1Id = Guid.NewGuid().ToString();
var master2Id = Guid.NewGuid().ToString();
@@ -179,7 +179,7 @@ internal class ReportContractTests
}));
_reportContract = new ReportContract(_serviceStorageContract.Object, _masterStorageContract.Object, _orderStorageContract.Object, _salaryStorageContract.Object, _postStorageContract.Object, _baseWordBuilder.Object, _baseExcelBuilder.Object, _basePdfBuilder.Object, new Mock<ILogger>().Object);
// Настраиваем мок для _masterStorageContract.GetList с конкретным значением onlyActive: true
var mastersToReturn = new List<MasterDataModel>()
{
new(master1Id, "Иванов Иван Иванович", Guid.NewGuid().ToString(), DateTime.UtcNow.AddYears(-30), DateTime.UtcNow.AddYears(-5), false),
@@ -187,9 +187,7 @@ internal class ReportContractTests
};
_masterStorageContract.Setup(x => x.GetList(true, null, null, null, null, null)).Returns(mastersToReturn);
// Создаем ReportContract после настройки моков
_reportContract = new ReportContract(_serviceStorageContract.Object, _masterStorageContract.Object, _orderStorageContract.Object, _salaryStorageContract.Object, _postStorageContract.Object, _baseWordBuilder.Object, _baseExcelBuilder.Object, _basePdfBuilder.Object, new Mock<ILogger>().Object);
_reportContract = new ReportContract(_serviceStorageContract.Object, _masterStorageContract.Object, _orderStorageContract.Object, _salaryStorageContract.Object, _postStorageContract.Object, _baseWordBuilder.Object, _baseExcelBuilder.Object, _basePdfBuilder.Object, new Mock<ILogger>().Object);
//Act
var data = await _reportContract.GetDataSalaryByPeriodAsync(startDate, endDate, CancellationToken.None);
@@ -202,17 +200,15 @@ internal class ReportContractTests
Assert.That(master1Salary, Is.Not.Null, "Иванов Иван Иванович not found in results");
Assert.Multiple(() =>
{
Assert.That(master1Salary.TotalSalary, Is.EqualTo(2000)); // 1000 + 1000
Assert.That(master1Salary.TotalSalary, Is.EqualTo(2000));
Assert.That(master1Salary.FromPeriod, Is.EqualTo(startDate));
Assert.That(master1Salary.ToPeriod, Is.EqualTo(endDate));
});
var master2Salary = data.First(x => x.MasterFIO == "Петров Петр Петрович");
Assert.That(master2Salary, Is.Not.Null);
Assert.That(master2Salary.TotalSalary, Is.EqualTo(300)); // 100 + 200
Assert.That(master2Salary.TotalSalary, Is.EqualTo(300));
_salaryStorageContract.Verify(x => x.GetListAsync(It.IsAny<DateTime>(), It.IsAny<DateTime>(), It.IsAny<CancellationToken>()), Times.Once);
_masterStorageContract.Verify(x => x.GetList(true, null, null, null, null, null), Times.Once);
}
[Test]
public async Task GetDataSalaryByPeriod_WhenNoRecords_ShouldSuccess_Test()
{
@@ -227,16 +223,7 @@ internal class ReportContractTests
_salaryStorageContract.Verify(x => x.GetListAsync(It.IsAny<DateTime>(), It.IsAny<DateTime>(), It.IsAny<CancellationToken>()), Times.Once);
}
[Test]
public void GetDataSalaryByPeriod_WhenIncorrectDates_ShouldFail_Test()
{
//Arrange
var date = DateTime.UtcNow;
_reportContract = new ReportContract(_serviceStorageContract.Object, _masterStorageContract.Object, _orderStorageContract.Object, _salaryStorageContract.Object, _postStorageContract.Object, _baseWordBuilder.Object, _baseExcelBuilder.Object, _basePdfBuilder.Object, new Mock<ILogger>().Object);
//Act&Assert
Assert.That(async () => await _reportContract.GetDataSalaryByPeriodAsync(date, date, CancellationToken.None), Throws.TypeOf<IncorrectDatesException>());
Assert.That(async () => await _reportContract.GetDataSalaryByPeriodAsync(date, DateTime.UtcNow.AddDays(-1), CancellationToken.None), Throws.TypeOf<IncorrectDatesException>());
}
[Test]
public void GetDataSalaryByPeriod_WhenStorageThrowError_ShouldFail_Test()

View File

@@ -22,6 +22,8 @@ internal static class TwoFromTheCasketDbContextExtensions
dbContext.ExecuteSqlRaw("TRUNCATE \"Services\" CASCADE;");
public static void RemoveServiceHistoriesFromDatabase(this TwoFromTheCasketDbContext dbContext) =>
dbContext.ExecuteSqlRaw("TRUNCATE \"ServiceHistories\" CASCADE;");
public static void RemoveServiceOrdersFromDatabase(this TwoFromTheCasketDbContext dbContext) =>
dbContext.ExecuteSqlRaw("TRUNCATE \"ServiceOrders\" CASCADE;");
public static Master InsertMasterToDatabaseAndReturn(this TwoFromTheCasketDbContext dbContext, string? id = null, string fio = "test", string? postId = null, DateTime? birthDate = null, DateTime?
employmentDate = null, bool isDeleted = false)
{
@@ -132,4 +134,18 @@ employmentDate = null, bool isDeleted = false)
dbContext.SaveChanges();
return serviceHistory;
}
public static ServiceOrder InsertServiceOrderToDatabaseAndReturn(this TwoFromTheCasketDbContext dbContext, string orderId, string serviceId, string masterId, int timeOfWorking = 1)
{
var serviceOrder = new ServiceOrder()
{
OrderId = orderId,
ServiceId = serviceId,
MasterId = masterId,
TimeOfWorking = timeOfWorking
};
dbContext.ServiceOrders.Add(serviceOrder);
dbContext.SaveChanges();
return serviceOrder;
}
}

View File

@@ -1,7 +1,10 @@
using TwoFromTheCasketContratcs.ViewModels;
using TwoFromTheCasketContratcs.Enums;
using TwoFromTheCasketTest.Infrastructure;
using TwoFromTheCasketDatabase.Implementation;
using TwoFromTheCasketContratcs.DataModels;
using System.Net;
using Microsoft.EntityFrameworkCore;
namespace TwoFromTheCasketTest.WebApiControllersTests;
@@ -13,43 +16,84 @@ internal class ReportControllerTests : BaseWebApiControllerTest
{
TwoFromTheCasketDbContext.RemoveServiceHistoriesFromDatabase();
TwoFromTheCasketDbContext.RemoveServicesFromDatabase();
TwoFromTheCasketDbContext.RemoveServiceOrdersFromDatabase();
TwoFromTheCasketDbContext.RemoveSalariesFromDatabase();
TwoFromTheCasketDbContext.RemoveOrdersFromDatabase();
TwoFromTheCasketDbContext.RemoveMastersFromDatabase();
TwoFromTheCasketDbContext.RemovePostsFromDatabase();
}
[Test]
public async Task GetMasters_WhenHaveRecords_ShouldSuccess_Test()
public async Task GetServices_WhenHaveRecords_ShouldSuccess_Test()
{
//Arrange
var post = TwoFromTheCasketDbContext.InsertPostToDatabaseAndReturn();
var master1 = TwoFromTheCasketDbContext.InsertMasterToDatabaseAndReturn(fio: "Иванов Иван Иванович", postId: post.PostId);
var master2 = TwoFromTheCasketDbContext.InsertMasterToDatabaseAndReturn(fio: "Петров Петр Петрович", postId: post.PostId);
var master3 = TwoFromTheCasketDbContext.InsertMasterToDatabaseAndReturn(fio: "Сидоров Сидор Сидорович", postId: post.PostId);
// Создаем сервисы с уникальными именами
var service1 = TwoFromTheCasketDbContext.InsertServiceToDatabaseAndReturn(id: Guid.NewGuid().ToString(), serviceName: "Service 1", masterId: master1.Id);
var service2 = TwoFromTheCasketDbContext.InsertServiceToDatabaseAndReturn(id: Guid.NewGuid().ToString(), serviceName: "Service 2", masterId: master2.Id);
var service3 = TwoFromTheCasketDbContext.InsertServiceToDatabaseAndReturn(id: Guid.NewGuid().ToString(), serviceName: "Service 3", masterId: master3.Id);
// Создаем историю сервисов
TwoFromTheCasketDbContext.InsertServiceHistoryToDatabaseAndReturn(serviceId: service1.Id);
TwoFromTheCasketDbContext.InsertServiceHistoryToDatabaseAndReturn(serviceId: service2.Id);
TwoFromTheCasketDbContext.InsertServiceHistoryToDatabaseAndReturn(serviceId: service3.Id);
var service1 = TwoFromTheCasketDbContext.InsertServiceToDatabaseAndReturn(
id: Guid.NewGuid().ToString(),
serviceName: "Покраска стен",
masterId: master1.Id,
serviceType: ServiceType.Painting,
price: 800.0);
var service2 = TwoFromTheCasketDbContext.InsertServiceToDatabaseAndReturn(
id: Guid.NewGuid().ToString(),
serviceName: "Штукатурка",
masterId: master2.Id,
serviceType: ServiceType.Plastering,
price: 1500.0);
TwoFromTheCasketDbContext.ChangeTracker.Clear();
var serviceDataModel1 = new ServiceDataModel(service1.Id, service1.ServiceName, service1.ServiceType, service1.MasterId, 900.0, service1.IsDeleted);
var serviceStorageContract = new ServiceStorageContract(TwoFromTheCasketDbContext);
serviceStorageContract.UpdElement(serviceDataModel1);
TwoFromTheCasketDbContext.ChangeTracker.Clear();
var serviceDataModel2 = new ServiceDataModel(service1.Id, service1.ServiceName, service1.ServiceType, service1.MasterId, 1000.0, service1.IsDeleted);
serviceStorageContract.UpdElement(serviceDataModel2);
TwoFromTheCasketDbContext.ChangeTracker.Clear();
var startDate = DateTime.SpecifyKind(DateTime.UtcNow.AddDays(-30), DateTimeKind.Utc);
var endDate = DateTime.SpecifyKind(DateTime.UtcNow.AddDays(1), DateTimeKind.Utc);
//Act
var response = await HttpClient.GetAsync("/api/Report/GetMasters");
var response = await HttpClient.GetAsync($"/api/Report/GetServices?fromDate={startDate:yyyy-MM-dd}&toDate={endDate:yyyy-MM-dd}");
//Assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var data = await GetModelFromResponseAsync<List<ServiceHistoryViewModel>>(response);
var data = await GetModelFromResponseAsync<List<ServiceWithHistoryViewModel>>(response);
Assert.That(data, Is.Not.Null);
Assert.Multiple(() =>
{
Assert.That(data, Has.Count.EqualTo(3));
Assert.That(data, Has.Count.EqualTo(2));
Assert.That(data.Any(s => s.ServiceName == "Покраска стен"), Is.True);
Assert.That(data.Any(s => s.ServiceName == "Штукатурка"), Is.True);
// Проверяем, что у услуги "Покраска стен" есть история изменений
var paintingService = data.First(s => s.ServiceName == "Покраска стен");
Assert.That(paintingService.History, Has.Count.GreaterThan(0));
Assert.That(paintingService.Price, Is.EqualTo(1000.0));
});
}
[Test]
public async Task GetServices_WhenDateIsIncorrect_ShouldBadRequest_Test()
{
//Act
var response = await HttpClient.GetAsync($"/api/Report/GetServices?fromDate={DateTime.UtcNow.AddDays(1):yyyy-MM-dd}&toDate={DateTime.UtcNow.AddDays(-1):yyyy-MM-dd}");
//Assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
}
[Test]
public async Task GetOrders_WhenHaveRecords_ShouldSuccess_Test()
{
@@ -118,11 +162,23 @@ internal class ReportControllerTests : BaseWebApiControllerTest
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
var data = await GetModelFromResponseAsync<List<MasterSalaryByPeriodViewModel>>(response);
Assert.That(data, Is.Not.Null);
Assert.That(data, Has.Count.EqualTo(2));
Assert.That(data, Has.Count.EqualTo(3));
Assert.Multiple(() =>
{
Assert.That(data.First(x => x.MasterFIO == master1.FIO).TotalSalary, Is.EqualTo(1000));
Assert.That(data.First(x => x.MasterFIO == master2.FIO).TotalSalary, Is.EqualTo(800));
Assert.That(data.Any(x => x.MasterFIO == master1.FIO), Is.True);
Assert.That(data.Any(x => x.MasterFIO == master2.FIO), Is.True);
var master1Salaries = data.Where(x => x.MasterFIO == master1.FIO).ToList();
var master2Salaries = data.Where(x => x.MasterFIO == master2.FIO).ToList();
Assert.That(master1Salaries.Count, Is.EqualTo(1));
Assert.That(master2Salaries.Count, Is.EqualTo(2));
Assert.That(master1Salaries.First().TotalSalary, Is.EqualTo(1000));
Assert.That(master2Salaries.Any(x => x.TotalSalary == 500), Is.True);
Assert.That(master2Salaries.Any(x => x.TotalSalary == 300), Is.True);
});
}
@@ -136,16 +192,57 @@ internal class ReportControllerTests : BaseWebApiControllerTest
}
[Test]
public async Task LoadMasters_WhenHaveRecords_ShouldSuccess_Test()
public async Task LoadServices_WhenHaveRecords_ShouldSuccess_Test()
{
// Arrange
var post = TwoFromTheCasketDbContext.InsertPostToDatabaseAndReturn();
var master1 = TwoFromTheCasketDbContext.InsertMasterToDatabaseAndReturn(fio: "Иванов Иван Иванович", postId: post.PostId);
var master2 = TwoFromTheCasketDbContext.InsertMasterToDatabaseAndReturn(fio: "Петров Петр Петрович", postId: post.PostId);
// Создаем сервис с начальной ценой
var service1 = TwoFromTheCasketDbContext.InsertServiceToDatabaseAndReturn(
id: Guid.NewGuid().ToString(),
serviceName: "Покраска стен",
masterId: master1.Id,
serviceType: ServiceType.Painting,
price: 800.0);
var service2 = TwoFromTheCasketDbContext.InsertServiceToDatabaseAndReturn(
id: Guid.NewGuid().ToString(),
serviceName: "Штукатурка",
masterId: master2.Id,
serviceType: ServiceType.Plastering,
price: 1500.0);
TwoFromTheCasketDbContext.ChangeTracker.Clear();
var serviceDataModel1 = new ServiceDataModel(service1.Id, service1.ServiceName, service1.ServiceType, service1.MasterId, 900.0, service1.IsDeleted);
var serviceStorageContract = new ServiceStorageContract(TwoFromTheCasketDbContext);
serviceStorageContract.UpdElement(serviceDataModel1);
TwoFromTheCasketDbContext.ChangeTracker.Clear();
var startDate = DateTime.SpecifyKind(DateTime.UtcNow.AddDays(-30), DateTimeKind.Utc);
var endDate = DateTime.SpecifyKind(DateTime.UtcNow.AddDays(1), DateTimeKind.Utc);
//Act
var response = await HttpClient.GetAsync($"/api/Report/LoadMasters?fromDate={DateTime.UtcNow.AddDays(-1):yyyy-MM-dd}&toDate={DateTime.UtcNow.AddDays(1):yyyy-MM-dd}");
var response = await HttpClient.GetAsync($"/api/Report/LoadServices?fromDate={startDate:yyyy-MM-dd}&toDate={endDate:yyyy-MM-dd}");
//Assert
await AssertStreamAsync(response, "masters.docx");
await AssertStreamAsync(response, "services_with_history.docx");
}
[Test]
public async Task LoadServices_WhenDateIsIncorrect_ShouldBadRequest_Test()
{
//Act
var response = await HttpClient.GetAsync($"/api/Report/LoadServices?fromDate={DateTime.UtcNow.AddDays(1):yyyy-MM-dd}&toDate={DateTime.UtcNow.AddDays(-1):yyyy-MM-dd}");
//Assert
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
}
[Test]
@@ -156,8 +253,69 @@ internal class ReportControllerTests : BaseWebApiControllerTest
var today = DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc);
var tomorrow = DateTime.SpecifyKind(DateTime.UtcNow.AddDays(1), DateTimeKind.Utc);
TwoFromTheCasketDbContext.InsertOrderToDatabaseAndReturn(id: Guid.NewGuid().ToString(), date: yesterday);
TwoFromTheCasketDbContext.InsertOrderToDatabaseAndReturn(id: Guid.NewGuid().ToString(), date: today);
var post1 = TwoFromTheCasketDbContext.InsertPostToDatabaseAndReturn();
var post2 = TwoFromTheCasketDbContext.InsertPostToDatabaseAndReturn();
var master1 = TwoFromTheCasketDbContext.InsertMasterToDatabaseAndReturn(fio: "Иванов Иван Иванович", postId: post1.PostId);
var master2 = TwoFromTheCasketDbContext.InsertMasterToDatabaseAndReturn(fio: "Петров Петр Петрович", postId: post2.PostId);
var service1 = TwoFromTheCasketDbContext.InsertServiceToDatabaseAndReturn(
id: Guid.NewGuid().ToString(),
serviceName: "Покраска стен",
masterId: master1.Id,
serviceType: ServiceType.Painting,
price: 800.0);
var service2 = TwoFromTheCasketDbContext.InsertServiceToDatabaseAndReturn(
id: Guid.NewGuid().ToString(),
serviceName: "Штукатурка",
masterId: master2.Id,
serviceType: ServiceType.Plastering,
price: 1500.0);
var service3 = TwoFromTheCasketDbContext.InsertServiceToDatabaseAndReturn(
id: Guid.NewGuid().ToString(),
serviceName: "Укладка плитки",
masterId: master1.Id,
serviceType: ServiceType.Carpentry,
price: 2000.0);
var order1 = TwoFromTheCasketDbContext.InsertOrderToDatabaseAndReturn(
id: Guid.NewGuid().ToString(),
date: yesterday,
status: StatusType.InProcess,
roomType: RoomType.Residential);
var order2 = TwoFromTheCasketDbContext.InsertOrderToDatabaseAndReturn(
id: Guid.NewGuid().ToString(),
date: today,
status: StatusType.InProcess,
roomType: RoomType.Industrial);
TwoFromTheCasketDbContext.InsertServiceOrderToDatabaseAndReturn(
orderId: order1.Id,
serviceId: service1.Id,
masterId: master1.Id,
timeOfWorking: 4);
TwoFromTheCasketDbContext.InsertServiceOrderToDatabaseAndReturn(
orderId: order1.Id,
serviceId: service2.Id,
masterId: master2.Id,
timeOfWorking: 6);
TwoFromTheCasketDbContext.InsertServiceOrderToDatabaseAndReturn(
orderId: order2.Id,
serviceId: service3.Id,
masterId: master1.Id,
timeOfWorking: 8);
TwoFromTheCasketDbContext.InsertServiceOrderToDatabaseAndReturn(
orderId: order2.Id,
serviceId: service1.Id,
masterId: master1.Id,
timeOfWorking: 3);
//Act
var response = await HttpClient.GetAsync($"/api/Report/LoadOrders?fromDate={yesterday:yyyy-MM-dd}&toDate={tomorrow:yyyy-MM-dd}");
//Assert
@@ -206,6 +364,9 @@ internal class ReportControllerTests : BaseWebApiControllerTest
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
}
private static async Task AssertStreamAsync(HttpResponseMessage response, string fileNameForSave = "")
{
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
@@ -224,7 +385,23 @@ internal class ReportControllerTests : BaseWebApiControllerTest
var path = Path.Combine(Directory.GetCurrentDirectory(), fileName);
if (File.Exists(path))
{
File.Delete(path);
try
{
File.Delete(path);
}
catch (IOException)
{
await Task.Delay(100);
try
{
File.Delete(path);
}
catch (IOException)
{
}
}
}
stream.Position = 0;
using var fileStream = new FileStream(path, FileMode.OpenOrCreate);

View File

@@ -25,6 +25,7 @@ public class ReportAdapter : IReportAdapter
cfg.CreateMap<ServiceHistoryDataModel, ServiceHistoryViewModel>();
cfg.CreateMap<OrderDataModel, OrderViewModel>();
cfg.CreateMap<MasterSalaryByPeriodDataModel, MasterSalaryByPeriodViewModel>();
cfg.CreateMap<ServiceWithHistoryDataModel, ServiceWithHistoryViewModel>();
});
_mapper = new Mapper(config);
}
@@ -57,30 +58,6 @@ public class ReportAdapter : IReportAdapter
}
}
public async Task<ReportOperationResponse> CreateDocumentMastersWithHistoryAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct)
{
try
{
var stream = await _reportContract.CreateDocumentServicesWithHistoryAsync(dateStart, dateFinish, ct);
return SendStream(stream, "service_history.docx");
}
catch (InvalidOperationException ex)
{
_logger.LogError(ex, "InvalidOperationException");
return ReportOperationResponse.InternalServerError($"Error while working with data storage: {ex.InnerException?.Message}");
}
catch (StorageException ex)
{
_logger.LogError(ex, "StorageException");
return ReportOperationResponse.InternalServerError($"Error while working with data storage: {ex.InnerException?.Message}");
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception");
return ReportOperationResponse.InternalServerError(ex.Message);
}
}
public async Task<ReportOperationResponse> CreateDocumentSalaryByPeriodAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct)
{
try
@@ -139,30 +116,6 @@ public class ReportAdapter : IReportAdapter
}
}
public async Task<ReportOperationResponse> GetDataMastersWithHistoryAsync(CancellationToken ct)
{
try
{
var histories = await _reportContract.GetDataServiceHistoryAsync(ct);
return ReportOperationResponse.OK(histories.Select(x => _mapper.Map<ServiceHistoryViewModel>(x)).ToList());
}
catch (InvalidOperationException ex)
{
_logger.LogError(ex, "InvalidOperationException");
return ReportOperationResponse.InternalServerError($"Error while working with data storage: {ex.InnerException?.Message}");
}
catch (StorageException ex)
{
_logger.LogError(ex, "StorageException");
return ReportOperationResponse.InternalServerError($"Error while working with data storage: {ex.InnerException?.Message}");
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception");
return ReportOperationResponse.InternalServerError(ex.Message);
}
}
public async Task<ReportOperationResponse> GetDataSalaryByPeriodAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct)
{
try
@@ -192,6 +145,65 @@ public class ReportAdapter : IReportAdapter
}
}
public async Task<ReportOperationResponse> GetDataServicesWithHistoryAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct)
{
try
{
var data = await _reportContract.GetDataServicesWithHistoryAsync(dateStart, dateFinish, ct);
var viewModels = data.Select(x => _mapper.Map<ServiceWithHistoryViewModel>(x)).ToList();
return ReportOperationResponse.OK(viewModels);
}
catch (IncorrectDatesException ex)
{
_logger.LogError(ex, "IncorrectDatesException in GetDataServicesWithHistoryAsync");
return ReportOperationResponse.BadRequest($"Incorrect dates: {ex.Message}");
}
catch (InvalidOperationException ex)
{
_logger.LogError(ex, "InvalidOperationException in GetDataServicesWithHistoryAsync: {message}", ex.Message);
return ReportOperationResponse.InternalServerError($"Error while working with data storage: {ex.InnerException?.Message}");
}
catch (StorageException ex)
{
_logger.LogError(ex, "StorageException in GetDataServicesWithHistoryAsync: {message}", ex.Message);
return ReportOperationResponse.InternalServerError($"Error while working with data storage: {ex.InnerException?.Message}");
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception in GetDataServicesWithHistoryAsync: {message}", ex.Message);
return ReportOperationResponse.InternalServerError(ex.Message);
}
}
public async Task<ReportOperationResponse> CreateDocumentServicesWithHistoryAsync(DateTime dateStart, DateTime dateFinish, CancellationToken ct)
{
try
{
var stream = await _reportContract.CreateDocumentServicesWithHistoryAsync(dateStart, dateFinish, ct);
return SendStream(stream, "services_with_history.docx");
}
catch (IncorrectDatesException ex)
{
_logger.LogError(ex, "IncorrectDatesException in CreateDocumentServicesWithHistoryAsync");
return ReportOperationResponse.BadRequest($"Incorrect dates: {ex.Message}");
}
catch (InvalidOperationException ex)
{
_logger.LogError(ex, "InvalidOperationException in CreateDocumentServicesWithHistoryAsync: {message}", ex.Message);
return ReportOperationResponse.InternalServerError($"Error while working with data storage: {ex.InnerException?.Message}");
}
catch (StorageException ex)
{
_logger.LogError(ex, "StorageException in CreateDocumentServicesWithHistoryAsync: {message}", ex.Message);
return ReportOperationResponse.InternalServerError($"Error while working with data storage: {ex.InnerException?.Message}");
}
catch (Exception ex)
{
_logger.LogError(ex, "Exception in CreateDocumentServicesWithHistoryAsync: {message}", ex.Message);
return ReportOperationResponse.InternalServerError(ex.Message);
}
}
private static ReportOperationResponse SendStream(Stream stream, string fileName)
{
stream.Position = 0;

View File

@@ -13,9 +13,11 @@ public class ReportController(IReportAdapter adapter) : ControllerBase
[HttpGet]
[Consumes("application/json")]
public async Task<IActionResult> GetMasters(CancellationToken ct)
public async Task<IActionResult> GetServices(DateTime fromDate, DateTime toDate, CancellationToken cancellationToken)
{
return (await _adapter.GetDataMastersWithHistoryAsync(ct)).GetResponse(Request, Response);
var utcFromDate = DateTime.SpecifyKind(fromDate, DateTimeKind.Utc);
var utcToDate = DateTime.SpecifyKind(toDate, DateTimeKind.Utc);
return (await _adapter.GetDataServicesWithHistoryAsync(utcFromDate, utcToDate, cancellationToken)).GetResponse(Request, Response);
}
[HttpGet]
@@ -38,11 +40,11 @@ public class ReportController(IReportAdapter adapter) : ControllerBase
[HttpGet]
[Consumes("application/octet-stream")]
public async Task<IActionResult> LoadMasters(DateTime fromDate, DateTime toDate, CancellationToken cancellationToken)
public async Task<IActionResult> LoadServices(DateTime fromDate, DateTime toDate, CancellationToken cancellationToken)
{
var utcFromDate = DateTime.SpecifyKind(fromDate, DateTimeKind.Utc);
var utcToDate = DateTime.SpecifyKind(toDate, DateTimeKind.Utc);
return (await _adapter.CreateDocumentMastersWithHistoryAsync(utcFromDate, utcToDate, cancellationToken)).GetResponse(Request, Response);
return (await _adapter.CreateDocumentServicesWithHistoryAsync(utcFromDate, utcToDate, cancellationToken)).GetResponse(Request, Response);
}
[HttpGet]