Utopia-Canteen-System/Services/MealMenuCacheSyncService.cs

443 lines
16 KiB
C#

using Microsoft.EntityFrameworkCore;
using MySqlConnector;
using UtopiaCanteenSystem.Data;
using UtopiaCanteenSystem.Models;
namespace UtopiaCanteenSystem.Services;
/// <summary>
/// Pulls meal_schedule, lunch_menu_week, lunch_menu_item, and menu_item from HRMS into SQLite.
/// </summary>
public class MealMenuCacheSyncService : IMealMenuCacheSyncService
{
private readonly IDbContextFactory<AppDbContext> _dbFactory;
private readonly IConfigService _configService;
public MealMenuCacheSyncService(IDbContextFactory<AppDbContext> dbFactory, IConfigService configService)
{
_dbFactory = dbFactory;
_configService = configService;
}
public async Task<MealMenuCacheSyncResult> SyncAllAsync(CancellationToken cancellationToken = default)
{
var connectionString = _configService.GetHrmsLookupConnectionString();
if (string.IsNullOrWhiteSpace(connectionString))
{
return new MealMenuCacheSyncResult
{
Success = false,
ErrorMessage = "HRMS lookup connection string is not configured."
};
}
try
{
var syncedAt = DateTime.UtcNow;
await using var db = await _dbFactory.CreateDbContextAsync(cancellationToken).ConfigureAwait(false);
var menuItems = await FetchMenuItemsAsync(connectionString, cancellationToken).ConfigureAwait(false);
var mealSchedules = await FetchMealSchedulesAsync(connectionString, cancellationToken).ConfigureAwait(false);
var lunchWeeks = await FetchLunchMenuWeeksAsync(connectionString, cancellationToken).ConfigureAwait(false);
var lunchItems = await FetchLunchMenuItemsAsync(connectionString, cancellationToken).ConfigureAwait(false);
var menuItemCount = await UpsertMenuItemsAsync(db, menuItems, syncedAt, cancellationToken).ConfigureAwait(false);
var mealScheduleCount = await UpsertMealSchedulesAsync(db, mealSchedules, syncedAt, cancellationToken).ConfigureAwait(false);
var lunchWeekCount = await UpsertLunchMenuWeeksAsync(db, lunchWeeks, syncedAt, cancellationToken).ConfigureAwait(false);
var lunchItemCount = await UpsertLunchMenuItemsAsync(db, lunchItems, syncedAt, cancellationToken).ConfigureAwait(false);
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
_configService.SetLastMealMenuCacheSyncUtc(syncedAt);
return new MealMenuCacheSyncResult
{
Success = true,
MealScheduleCount = mealScheduleCount,
LunchMenuWeekCount = lunchWeekCount,
LunchMenuItemCount = lunchItemCount,
MenuItemCount = menuItemCount
};
}
catch (Exception ex)
{
Logger.Log(ex, "MealMenuCacheSyncService.SyncAllAsync");
return new MealMenuCacheSyncResult
{
Success = false,
ErrorMessage = ex.Message
};
}
}
private static async Task<int> UpsertMenuItemsAsync(
AppDbContext db,
List<MenuItemRow> rows,
DateTime syncedAt,
CancellationToken cancellationToken)
{
var count = 0;
foreach (var row in rows)
{
if (row.HrmsId <= 0)
continue;
var existing = await db.MenuItemCache
.FirstOrDefaultAsync(x => x.HrmsId == row.HrmsId, cancellationToken)
.ConfigureAwait(false);
if (existing == null)
{
db.MenuItemCache.Add(new MenuItemCache
{
HrmsId = row.HrmsId,
ItemName = row.ItemName,
ItemType = row.ItemType,
Price = row.Price,
ItemFor = row.ItemFor,
LocationSiteId = row.LocationSiteId,
LastSyncedAtUtc = syncedAt
});
}
else
{
existing.ItemName = row.ItemName;
existing.ItemType = row.ItemType;
existing.Price = row.Price;
existing.ItemFor = row.ItemFor;
existing.LocationSiteId = row.LocationSiteId;
existing.LastSyncedAtUtc = syncedAt;
}
count++;
}
return count;
}
private static async Task<int> UpsertMealSchedulesAsync(
AppDbContext db,
List<MealScheduleRow> rows,
DateTime syncedAt,
CancellationToken cancellationToken)
{
var count = 0;
foreach (var row in rows)
{
if (row.HrmsId <= 0)
continue;
var existing = await db.MealScheduleCache
.FirstOrDefaultAsync(x => x.HrmsId == row.HrmsId, cancellationToken)
.ConfigureAwait(false);
if (existing == null)
{
db.MealScheduleCache.Add(new MealScheduleCache
{
HrmsId = row.HrmsId,
MealName = row.MealName,
StartTime = row.StartTime,
EndTime = row.EndTime,
CreatedAt = row.CreatedAt,
UpdatedAt = row.UpdatedAt,
LocationSiteId = row.LocationSiteId,
LastSyncedAtUtc = syncedAt
});
}
else
{
existing.MealName = row.MealName;
existing.StartTime = row.StartTime;
existing.EndTime = row.EndTime;
existing.CreatedAt = row.CreatedAt;
existing.UpdatedAt = row.UpdatedAt;
existing.LocationSiteId = row.LocationSiteId;
existing.LastSyncedAtUtc = syncedAt;
}
count++;
}
return count;
}
private static async Task<int> UpsertLunchMenuWeeksAsync(
AppDbContext db,
List<LunchMenuWeekRow> rows,
DateTime syncedAt,
CancellationToken cancellationToken)
{
var count = 0;
foreach (var row in rows)
{
if (row.HrmsId <= 0)
continue;
var existing = await db.LunchMenuWeekCache
.FirstOrDefaultAsync(x => x.HrmsId == row.HrmsId, cancellationToken)
.ConfigureAwait(false);
if (existing == null)
{
db.LunchMenuWeekCache.Add(new LunchMenuWeekCache
{
HrmsId = row.HrmsId,
WeekStartDate = row.WeekStartDate,
WeekEndDate = row.WeekEndDate,
CreatedBy = row.CreatedBy,
CreatedAt = row.CreatedAt,
LocationSiteId = row.LocationSiteId,
LastSyncedAtUtc = syncedAt
});
}
else
{
existing.WeekStartDate = row.WeekStartDate;
existing.WeekEndDate = row.WeekEndDate;
existing.CreatedBy = row.CreatedBy;
existing.CreatedAt = row.CreatedAt;
existing.LocationSiteId = row.LocationSiteId;
existing.LastSyncedAtUtc = syncedAt;
}
count++;
}
return count;
}
private static async Task<int> UpsertLunchMenuItemsAsync(
AppDbContext db,
List<LunchMenuItemRow> rows,
DateTime syncedAt,
CancellationToken cancellationToken)
{
var count = 0;
foreach (var row in rows)
{
if (row.HrmsId <= 0)
continue;
var existing = await db.LunchMenuItemCache
.FirstOrDefaultAsync(x => x.HrmsId == row.HrmsId, cancellationToken)
.ConfigureAwait(false);
if (existing == null)
{
db.LunchMenuItemCache.Add(new LunchMenuItemCache
{
HrmsId = row.HrmsId,
LunchMenuWeekHrmsId = row.LunchMenuWeekHrmsId,
DayOfWeek = row.DayOfWeek,
MealName = row.MealName,
MenuItemHrmsId = row.MenuItemHrmsId,
CreatedAt = row.CreatedAt,
MenuDate = row.MenuDate,
LastSyncedAtUtc = syncedAt
});
}
else
{
existing.LunchMenuWeekHrmsId = row.LunchMenuWeekHrmsId;
existing.DayOfWeek = row.DayOfWeek;
existing.MealName = row.MealName;
existing.MenuItemHrmsId = row.MenuItemHrmsId;
existing.CreatedAt = row.CreatedAt;
existing.MenuDate = row.MenuDate;
existing.LastSyncedAtUtc = syncedAt;
}
count++;
}
return count;
}
private static async Task<List<MealScheduleRow>> FetchMealSchedulesAsync(string connectionString, CancellationToken cancellationToken)
{
const string sql = @"
SELECT id, meal_name, start_time, end_time, created_at, updated_at, location_site_id
FROM meal_schedule";
return await QueryRowsAsync(connectionString, sql, cancellationToken, reader => new MealScheduleRow
{
HrmsId = GetInt64(reader, 0),
MealName = GetString(reader, 1),
StartTime = GetTimeString(reader, 2),
EndTime = GetTimeString(reader, 3),
CreatedAt = GetDateTimeNullable(reader, 4),
UpdatedAt = GetDateTimeNullable(reader, 5),
LocationSiteId = GetInt(reader, 6)
}).ConfigureAwait(false);
}
private static async Task<List<LunchMenuWeekRow>> FetchLunchMenuWeeksAsync(string connectionString, CancellationToken cancellationToken)
{
const string sql = @"
SELECT id, week_start_date, week_end_date, created_by, created_at, location_site_id
FROM lunch_menu_week";
return await QueryRowsAsync(connectionString, sql, cancellationToken, reader => new LunchMenuWeekRow
{
HrmsId = GetInt64(reader, 0),
WeekStartDate = GetDateTimeNullable(reader, 1),
WeekEndDate = GetDateTimeNullable(reader, 2),
CreatedBy = GetString(reader, 3),
CreatedAt = GetDateTimeNullable(reader, 4),
LocationSiteId = GetInt(reader, 5)
}).ConfigureAwait(false);
}
private static async Task<List<LunchMenuItemRow>> FetchLunchMenuItemsAsync(string connectionString, CancellationToken cancellationToken)
{
const string sql = @"
SELECT id, lunch_menu_week_id, day_of_week, meal_name, menu_item_id, created_at, menu_date
FROM lunch_menu_item";
return await QueryRowsAsync(connectionString, sql, cancellationToken, reader => new LunchMenuItemRow
{
HrmsId = GetInt64(reader, 0),
LunchMenuWeekHrmsId = GetInt64(reader, 1),
DayOfWeek = GetString(reader, 2),
MealName = GetString(reader, 3),
MenuItemHrmsId = GetInt64(reader, 4),
CreatedAt = GetDateTimeNullable(reader, 5),
MenuDate = GetDateTimeNullable(reader, 6)
}).ConfigureAwait(false);
}
private static async Task<List<MenuItemRow>> FetchMenuItemsAsync(string connectionString, CancellationToken cancellationToken)
{
const string sql = @"
SELECT id, item_name, item_type, price, item_for, location_site_id
FROM menu_item";
return await QueryRowsAsync(connectionString, sql, cancellationToken, reader => new MenuItemRow
{
HrmsId = GetInt64(reader, 0),
ItemName = GetString(reader, 1),
ItemType = GetString(reader, 2),
Price = GetDecimal(reader, 3),
ItemFor = GetString(reader, 4),
LocationSiteId = GetInt(reader, 5)
}).ConfigureAwait(false);
}
private static async Task<List<T>> QueryRowsAsync<T>(
string connectionString,
string sql,
CancellationToken cancellationToken,
Func<MySqlDataReader, T> map)
{
var rows = new List<T>();
await using var conn = new MySqlConnection(connectionString);
await conn.OpenAsync(cancellationToken).ConfigureAwait(false);
await using var cmd = new MySqlCommand(sql, conn);
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
rows.Add(map(reader));
return rows;
}
private static string GetString(MySqlDataReader reader, int ordinal)
{
if (reader.IsDBNull(ordinal))
return string.Empty;
return reader.GetValue(ordinal)?.ToString() ?? string.Empty;
}
private static int GetInt(MySqlDataReader reader, int ordinal)
{
if (reader.IsDBNull(ordinal))
return 0;
var v = reader.GetValue(ordinal);
if (v is int i)
return i;
if (v is long l)
return (int)l;
return int.TryParse(v?.ToString(), out var parsed) ? parsed : 0;
}
private static long GetInt64(MySqlDataReader reader, int ordinal)
{
if (reader.IsDBNull(ordinal))
return 0;
var v = reader.GetValue(ordinal);
if (v is long l)
return l;
if (v is int i)
return i;
return long.TryParse(v?.ToString(), out var parsed) ? parsed : 0;
}
private static decimal GetDecimal(MySqlDataReader reader, int ordinal)
{
if (reader.IsDBNull(ordinal))
return 0m;
var v = reader.GetValue(ordinal);
return v is decimal d ? d : Convert.ToDecimal(v);
}
private static DateTime? GetDateTimeNullable(MySqlDataReader reader, int ordinal)
{
if (reader.IsDBNull(ordinal))
return null;
var v = reader.GetValue(ordinal);
if (v is DateTime dt)
return dt;
return DateTime.TryParse(v?.ToString(), out var parsed) ? parsed : null;
}
private static string GetTimeString(MySqlDataReader reader, int ordinal)
{
if (reader.IsDBNull(ordinal))
return "00:00:00";
var v = reader.GetValue(ordinal);
if (v is TimeSpan ts)
return ts.ToString(@"hh\:mm\:ss");
return v?.ToString()?.Trim() ?? "00:00:00";
}
private sealed class MealScheduleRow
{
public long HrmsId { get; set; }
public string MealName { get; set; } = string.Empty;
public string StartTime { get; set; } = string.Empty;
public string EndTime { get; set; } = string.Empty;
public DateTime? CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
public int LocationSiteId { get; set; }
}
private sealed class LunchMenuWeekRow
{
public long HrmsId { get; set; }
public DateTime? WeekStartDate { get; set; }
public DateTime? WeekEndDate { get; set; }
public string CreatedBy { get; set; } = string.Empty;
public DateTime? CreatedAt { get; set; }
public int LocationSiteId { get; set; }
}
private sealed class LunchMenuItemRow
{
public long HrmsId { get; set; }
public long LunchMenuWeekHrmsId { get; set; }
public string DayOfWeek { get; set; } = string.Empty;
public string MealName { get; set; } = string.Empty;
public long MenuItemHrmsId { get; set; }
public DateTime? CreatedAt { get; set; }
public DateTime? MenuDate { get; set; }
}
private sealed class MenuItemRow
{
public long HrmsId { get; set; }
public string ItemName { get; set; } = string.Empty;
public string ItemType { get; set; } = string.Empty;
public decimal Price { get; set; }
public string ItemFor { get; set; } = string.Empty;
public int LocationSiteId { get; set; }
}
}