fix: отображение количества участников лекции
🚀 Create and publish a Docker image / Detect changes in backend and frontend (push) Successful in 11s
🚀 Create and publish a Docker image / Build & publish backend image (push) Successful in 6m5s
🚀 Create and publish a Docker image / Build & publish frontend image (push) Successful in 29s
🚀 Create and publish a Docker image / Update stack on Portainer (push) Successful in 9s

This commit is contained in:
2026-05-13 19:49:51 +03:00
parent f6aaf0b923
commit 65e3d1bf18
11 changed files with 208 additions and 19 deletions
@@ -0,0 +1,133 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using UniVerse.Application.DTOs.Sync;
using UniVerse.Application.Interfaces;
using UniVerse.Infrastructure.Data;
using UniVerse.Infrastructure.Services;
using Xunit;
namespace UniVerse.Api.Tests.Sync;
public class ScheduleSyncServiceTests
{
[Fact]
public async Task SyncScheduleAsync_UsesRoomWorkingCapacityForLectureSeats()
{
await using var db = CreateDbContext();
var modeus = Substitute.For<IModeusApiClient>();
modeus.SearchEventsAsync(Arg.Any<SyncScheduleRequest>())
.Returns(new ModeusEventsResponse
{
Embedded = new ModeusEventsEmbedded
{
Events =
[
new ModeusEvent
{
Id = "event-1",
Name = "Open lecture",
StartsAt = new DateTime(2026, 5, 13, 9, 0, 0, DateTimeKind.Utc),
EndsAt = new DateTime(2026, 5, 13, 10, 30, 0, DateTimeKind.Utc)
}
],
EventRooms =
[
new ModeusEventRoom
{
Links = new ModeusEventRoomLinks
{
Event = new ModeusHrefLink("/events/event-1"),
Room = new ModeusHrefLink("/rooms/room-1")
}
}
],
Rooms =
[
new ModeusRoom("room-1", "Room 101", "101", null, TotalCapacity: 60, WorkingCapacity: 42)
],
EventTeams =
[
new ModeusEventTeam("event-1", 15)
]
}
});
var service = new ScheduleSyncService(db, modeus, NullLogger<ScheduleSyncService>.Instance);
var result = await service.SyncScheduleAsync(new SyncScheduleRequest(null, null, null, null));
var lecture = await db.Lectures.SingleAsync();
Assert.Null(result.Error);
Assert.Equal(1, result.Created);
Assert.Equal(42, lecture.MaxEnrollments);
}
[Fact]
public async Task SyncScheduleAsync_LoadsRoomCapacityWhenEventRoomHasNoCapacity()
{
await using var db = CreateDbContext();
var modeus = Substitute.For<IModeusApiClient>();
modeus.SearchEventsAsync(Arg.Any<SyncScheduleRequest>())
.Returns(new ModeusEventsResponse
{
Embedded = new ModeusEventsEmbedded
{
Events =
[
new ModeusEvent
{
Id = "event-1",
Name = "Open lecture",
StartsAt = new DateTime(2026, 5, 13, 9, 0, 0, DateTimeKind.Utc),
EndsAt = new DateTime(2026, 5, 13, 10, 30, 0, DateTimeKind.Utc)
}
],
EventRooms =
[
new ModeusEventRoom
{
Links = new ModeusEventRoomLinks
{
Event = new ModeusHrefLink("/events/event-1"),
Room = new ModeusHrefLink("/rooms/room-1")
}
}
],
Rooms =
[
new ModeusRoom("room-1", "Room 101", "101", null, TotalCapacity: null, WorkingCapacity: null)
],
EventTeams =
[
new ModeusEventTeam("event-1", 15)
]
}
});
modeus.SearchRoomsAsync()
.Returns(new ModeusRoomsResponse
{
Rooms =
[
new ModeusRoom("room-1", "Room 101", "101", null, TotalCapacity: 60, WorkingCapacity: 48)
]
});
var service = new ScheduleSyncService(db, modeus, NullLogger<ScheduleSyncService>.Instance);
var result = await service.SyncScheduleAsync(new SyncScheduleRequest(null, null, null, null));
var lecture = await db.Lectures.SingleAsync();
Assert.Null(result.Error);
Assert.Equal(1, result.Created);
Assert.Equal(48, lecture.MaxEnrollments);
}
private static AppDbContext CreateDbContext()
{
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase($"ScheduleSyncTests_{Guid.NewGuid()}")
.Options;
return new AppDbContext(options);
}
}
@@ -27,6 +27,9 @@ public class ScheduleSyncService : IScheduleSyncService
try
{
var events = await _modeus.SearchEventsAsync(request);
var embeddedRoomCapacityById = BuildRoomCapacityLookup(events.Embedded?.Rooms);
IReadOnlyDictionary<string, int>? syncedRoomCapacityById = null;
foreach (var ev in events.EventItems)
{
if (string.IsNullOrWhiteSpace(ev.Id) || string.IsNullOrWhiteSpace(ev.Name))
@@ -41,8 +44,15 @@ public class ScheduleSyncService : IScheduleSyncService
var courseExternalId = courseUnit?.Id ?? ev.TypeId ?? ev.Id;
var courseName = courseUnit?.Name ?? ev.Name;
var location = await UpsertEventLocationAsync(events, ev.Id);
var maxEnrollments = events.Embedded?.EventTeams?
.FirstOrDefault(team => team.EventId == ev.Id)?.Size ?? 0;
var roomId = GetEventRoomId(events, ev.Id);
var maxEnrollments = GetRoomCapacity(embeddedRoomCapacityById, roomId);
if (maxEnrollments is null && !string.IsNullOrWhiteSpace(roomId))
{
syncedRoomCapacityById ??= await LoadRoomCapacityLookupAsync();
maxEnrollments = GetRoomCapacity(syncedRoomCapacityById, roomId);
}
var lectureCapacity = maxEnrollments ?? GetEventTeamSize(events, ev.Id) ?? 0;
var startsAt = EnsureUtc(ev.StartsAt);
var endsAt = EnsureUtc(ev.EndsAt);
@@ -54,7 +64,7 @@ public class ScheduleSyncService : IScheduleSyncService
existing.StartsAt = startsAt;
existing.EndsAt = endsAt;
existing.LocationId = location?.Id;
existing.MaxEnrollments = maxEnrollments;
existing.MaxEnrollments = lectureCapacity;
existing.UpdatedAt = DateTime.UtcNow;
updated++;
}
@@ -77,7 +87,7 @@ public class ScheduleSyncService : IScheduleSyncService
ExternalId = ev.Id,
StartsAt = startsAt,
EndsAt = endsAt,
MaxEnrollments = maxEnrollments
MaxEnrollments = lectureCapacity
});
created++;
}
@@ -182,14 +192,7 @@ public class ScheduleSyncService : IScheduleSyncService
private async Task<Location?> UpsertEventLocationAsync(ModeusEventsResponse events, string eventId)
{
var roomId = events.Embedded?.EventRooms?
.Select(eventRoom => new
{
EventId = GetHrefId(eventRoom.Links?.Event?.Href),
RoomId = GetHrefId(eventRoom.Links?.Room?.Href)
})
.FirstOrDefault(link => link.EventId == eventId)
?.RoomId;
var roomId = GetEventRoomId(events, eventId);
if (string.IsNullOrWhiteSpace(roomId))
return null;
@@ -222,6 +225,55 @@ public class ScheduleSyncService : IScheduleSyncService
return location;
}
private static string? GetEventRoomId(ModeusEventsResponse events, string eventId) =>
events.Embedded?.EventRooms?
.Select(eventRoom => new
{
EventId = GetHrefId(eventRoom.Links?.Event?.Href),
RoomId = GetHrefId(eventRoom.Links?.Room?.Href)
})
.FirstOrDefault(link => link.EventId == eventId)
?.RoomId;
private async Task<IReadOnlyDictionary<string, int>> LoadRoomCapacityLookupAsync()
{
try
{
var rooms = await _modeus.SearchRoomsAsync();
return BuildRoomCapacityLookup(rooms.RoomItems);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Could not load room capacities from Modeus rooms search.");
return new Dictionary<string, int>();
}
}
private static IReadOnlyDictionary<string, int> BuildRoomCapacityLookup(IEnumerable<ModeusRoom>? rooms)
{
var result = new Dictionary<string, int>();
foreach (var room in rooms ?? [])
{
var capacity = NormalizeCapacity(room.WorkingCapacity) ?? NormalizeCapacity(room.TotalCapacity);
if (!string.IsNullOrWhiteSpace(room.Id) && capacity.HasValue)
result.TryAdd(room.Id, capacity.Value);
}
return result;
}
private static int? GetRoomCapacity(IReadOnlyDictionary<string, int> roomCapacityById, string? roomId) =>
!string.IsNullOrWhiteSpace(roomId) && roomCapacityById.TryGetValue(roomId, out var capacity)
? capacity
: null;
private static int? GetEventTeamSize(ModeusEventsResponse events, string eventId) =>
NormalizeCapacity(events.Embedded?.EventTeams?
.FirstOrDefault(team => team.EventId == eventId)?.Size);
private static int? NormalizeCapacity(int? capacity) =>
capacity is > 0 ? capacity : null;
private static IReadOnlyList<string> BuildErrorDetails(
Exception exception,
string stage,