Files
UniVerse/backend/UniVerse.Infrastructure/Services/ScheduleSyncService.cs
T

158 lines
6.2 KiB
C#

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using UniVerse.Application.DTOs.Sync;
using UniVerse.Application.Interfaces;
using UniVerse.Domain.Entities;
using UniVerse.Infrastructure.Data;
namespace UniVerse.Infrastructure.Services;
public class ScheduleSyncService : IScheduleSyncService
{
private readonly AppDbContext _db;
private readonly IModeusApiClient _modeus;
private readonly ILogger<ScheduleSyncService> _logger;
private static SyncStatusDto _lastStatus = new(null, "idle", null);
public ScheduleSyncService(AppDbContext db, IModeusApiClient modeus, ILogger<ScheduleSyncService> logger)
{
_db = db; _modeus = modeus; _logger = logger;
}
public async Task<SyncResultDto> SyncScheduleAsync(SyncScheduleRequest request)
{
const string stage = "schedule";
int created = 0, updated = 0, skipped = 0;
try
{
var events = await _modeus.SearchEventsAsync(request);
foreach (var ev in events.Events)
{
var existing = await _db.Lectures.FirstOrDefaultAsync(l => l.ExternalId == ev.Id);
if (existing != null) { updated++; existing.StartsAt = ev.StartsAt; existing.EndsAt = ev.EndsAt; existing.UpdatedAt = DateTime.UtcNow; }
else
{
var course = await _db.Courses.FirstOrDefaultAsync(c => c.ExternalId == ev.TypeId);
if (course == null) { course = new Course { Name = ev.Name, ExternalId = ev.TypeId, IsSynced = true }; _db.Courses.Add(course); await _db.SaveChangesAsync(); }
_db.Lectures.Add(new Lecture { CourseId = course.Id, Title = ev.Name, ExternalId = ev.Id, StartsAt = ev.StartsAt, EndsAt = ev.EndsAt });
created++;
}
}
await _db.SaveChangesAsync();
var result = new SyncResultDto(created, updated, skipped, null);
_lastStatus = new SyncStatusDto(DateTime.UtcNow, "completed", result);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Schedule sync failed");
var result = new SyncResultDto(created, updated, skipped, ex.Message, BuildErrorDetails(
ex,
stage,
created,
updated,
skipped,
[
$"specialtyCode={request.SpecialtyCode ?? "<empty>"}",
$"timeMin={request.TimeMin:O}",
$"timeMax={request.TimeMax:O}",
$"typeId=[{string.Join(", ", request.TypeId ?? [])}]"
]));
_lastStatus = new SyncStatusDto(DateTime.UtcNow, "failed", result);
return result;
}
}
public async Task<SyncResultDto> SyncRoomsAsync()
{
const string stage = "rooms";
int created = 0, updated = 0, skipped = 0;
try
{
var rooms = await _modeus.SearchRoomsAsync();
foreach (var room in rooms?.RoomItems ?? [])
{
if (room is null || string.IsNullOrWhiteSpace(room.Id) || string.IsNullOrWhiteSpace(room.Name))
{
skipped++;
continue;
}
var existing = await _db.Locations.FirstOrDefaultAsync(l => l.ExternalId == room.Id);
if (existing != null)
{
existing.Name = room.Name;
existing.Room = room.NameShort;
existing.Building = room.Building?.Name ?? room.Building?.NameShort;
existing.Address = room.Building?.Address;
updated++;
}
else
{
_db.Locations.Add(new Location
{
Name = room.Name,
Room = room.NameShort,
Building = room.Building?.Name ?? room.Building?.NameShort,
Address = room.Building?.Address,
ExternalId = room.Id
});
created++;
}
}
await _db.SaveChangesAsync();
var result = new SyncResultDto(created, updated, skipped, null);
_lastStatus = new SyncStatusDto(DateTime.UtcNow, "completed", result);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Rooms sync failed");
var result = new SyncResultDto(created, updated, skipped, ex.Message, BuildErrorDetails(
ex,
stage,
created,
updated,
skipped,
["request=name:<empty>, sort:+building.name,+name, deleted:false, page size:100"]));
_lastStatus = new SyncStatusDto(DateTime.UtcNow, "failed", result);
return result;
}
}
public async Task<List<EmployeeDto>> SearchEmployeesAsync(string fullname)
{
var employees = await _modeus.SearchEmployeeAsync(fullname);
return employees.Select(e => new EmployeeDto(e.Id, e.FullName, e.Department)).ToList();
}
public Task<SyncStatusDto> GetLastSyncStatusAsync() => Task.FromResult(_lastStatus);
private static IReadOnlyList<string> BuildErrorDetails(
Exception exception,
string stage,
int created,
int updated,
int skipped,
IReadOnlyList<string> context)
{
var details = new List<string>
{
$"stage={stage}",
$"exceptionType={exception.GetType().FullName}",
$"message={exception.Message}",
$"partialResult=created:{created}, updated:{updated}, skipped:{skipped}"
};
if (exception is HttpRequestException httpException && httpException.StatusCode.HasValue)
details.Add($"httpStatus={(int)httpException.StatusCode.Value} {httpException.StatusCode.Value}");
if (exception.InnerException != null)
details.Add($"innerException={exception.InnerException.GetType().FullName}: {exception.InnerException.Message}");
details.AddRange(context);
return details;
}
}