using System; using MySqlConnector; using Serilog; using SyncEngine.Configuration; namespace SyncEngine.MySql { public sealed class ShopSyncLogClient { private readonly ILogger _logger; private readonly SyncOptions _options; private readonly IShopSyncLogApiClient _apiClient; public ShopSyncLogClient(ILogger logger, SyncOptions options) : this(logger, options, options != null && options.HasShopSyncApiFallback ? new ShopSyncLogApiClient(options, logger) : null) { } internal ShopSyncLogClient(ILogger logger, SyncOptions options, IShopSyncLogApiClient apiClient) { _logger = logger; _options = options; _apiClient = apiClient; } /// MySQL INSERT only; throws on failure. public void WriteLog(ShopSyncLogEntry entry, MySqlConnectionFactory mysqlFactory) { if (MySqlWriteForTests != null) { MySqlWriteForTests(entry, mysqlFactory); _logger.Information("Inserted shop_sync_log row: {Status}", entry.Status); return; } using (var conn = mysqlFactory.Create()) { conn.Open(); var sql = @" INSERT INTO shop_sync_log (run_started_at, run_finished_at, status, source_reachable, products_upserted, products_withdrawn, message) VALUES (@start, @finish, @status, @reachable, @upserted, @withdrawn, @message)"; using (var cmd = new MySqlCommand(sql, conn)) { BindInsertParameters(cmd, entry); cmd.ExecuteNonQuery(); } } _logger.Information("Inserted shop_sync_log row: {Status}", entry.Status); } /// MySQL first; on any write failure, POST JSON to CI4 API when configured. public void WriteLogWithFallback(ShopSyncLogEntry entry, MySqlConnectionFactory mysqlFactory, SyncOptions options) { try { WriteLog(entry, mysqlFactory); } catch (Exception mysqlEx) { if (options == null || !options.HasShopSyncApiFallback) { throw; } var apiClient = _apiClient ?? new ShopSyncLogApiClient(options, _logger); try { apiClient.PostLog(entry); _logger.Information("shop_sync_log written via API fallback: {Status}", entry.Status); } catch (Exception apiEx) { var localPath = ShopSyncLogLocalWriter.WriteFailedApiPost(entry, mysqlEx, apiEx); _logger.Warning(apiEx, "shop-sync/log API fallback failed; run data written to {LocalPath}", localPath); throw new AggregateException( "Failed to write shop_sync_log via MySQL and API fallback.", mysqlEx, apiEx); } } } /// Test seam to simulate MySQL write success or failure without a live database. internal Action MySqlWriteForTests { get; set; } /// Maps entry fields to INSERT parameters (testable without a live connection). public static void BindInsertParameters(MySqlCommand cmd, ShopSyncLogEntry entry) { cmd.Parameters.AddWithValue("@start", entry.RunStartedAt); cmd.Parameters.AddWithValue("@finish", entry.RunFinishedAt); cmd.Parameters.AddWithValue("@status", entry.Status); cmd.Parameters.AddWithValue("@reachable", entry.SourceReachable ? 1 : 0); cmd.Parameters.AddWithValue("@upserted", entry.ProductsUpserted); cmd.Parameters.AddWithValue("@withdrawn", entry.ProductsWithdrawn); cmd.Parameters.AddWithValue("@message", (object)entry.Message ?? DBNull.Value); } } public sealed class ShopSyncLogEntry { public DateTime RunStartedAt { get; set; } public DateTime RunFinishedAt { get; set; } public string Status { get; set; } public bool SourceReachable { get; set; } public int ProductsUpserted { get; set; } public int ProductsWithdrawn { get; set; } public string Message { get; set; } } }