diff --git a/Services/CanteenBackendApiClient.cs b/Services/CanteenBackendApiClient.cs index b762920..6cf0a81 100644 --- a/Services/CanteenBackendApiClient.cs +++ b/Services/CanteenBackendApiClient.cs @@ -4,6 +4,7 @@ using System.Text.Json; using UtopiaCanteen.Shared; using UtopiaCanteenSystem.Api; using UtopiaCanteenSystem.Models; +using UtopiaCanteenSystem.Services.Logging; namespace UtopiaCanteenSystem.Services; @@ -32,15 +33,24 @@ public class CanteenBackendApiClient : ICanteenBackendApiClient public async Task HealthCheckAsync(CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(BaseUrl)) + { + FileLogger.Warn("ClientApi", "Health check skipped. Backend base URL is not configured."); return false; + } try { var url = $"{BaseUrl}/api/health"; var resp = await _http.GetFromJsonAsync(url, JsonOptions, cancellationToken).ConfigureAwait(false); - return resp != null && string.Equals(resp.Status, "ok", StringComparison.OrdinalIgnoreCase); + var ok = resp != null && string.Equals(resp.Status, "ok", StringComparison.OrdinalIgnoreCase); + if (ok) + FileLogger.Info("ClientApi", $"Health check succeeded. Url={BaseUrl}"); + else + FileLogger.Warn("ClientApi", $"Health check failed (unexpected response). Url={BaseUrl}"); + return ok; } catch (Exception ex) { + FileLogger.Error("ClientApi", $"Health check failed. Url={BaseUrl}", ex); Logger.Log(ex, "CanteenBackendApiClient.HealthCheckAsync"); return false; } @@ -79,6 +89,10 @@ public class CanteenBackendApiClient : ICanteenBackendApiClient if (string.IsNullOrWhiteSpace(BaseUrl)) return new ScanResult(false, "Backend URL is not configured.", 0); + FileLogger.Info( + "ClientApi", + $"API scan request started. CardId={LogMasking.MaskCardId(cardId)}, POST {BaseUrl}/api/rfid/scan"); + try { var url = $"{BaseUrl}/api/rfid/scan"; @@ -94,15 +108,20 @@ public class CanteenBackendApiClient : ICanteenBackendApiClient if (!response.IsSuccessStatusCode) { var err = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); - return new ScanResult(false, + var fail = new ScanResult(false, $"Backend error ({(int)response.StatusCode}): {Truncate(err, 200)}", 0); + FileLogger.Warn("ClientApi", $"API scan request failed. Success=false, Message={fail.Message}"); + return fail; } var dto = response.Content.ReadFromJsonAsync(JsonOptions).GetAwaiter().GetResult(); if (dto == null) + { + FileLogger.Warn("ClientApi", "API scan request failed. Invalid response from backend."); return new ScanResult(false, "Invalid response from backend.", 0); + } - return new ScanResult( + var scanResult = new ScanResult( dto.Success, dto.Message ?? string.Empty, dto.CooldownSecondsRemaining, @@ -110,9 +129,15 @@ public class CanteenBackendApiClient : ICanteenBackendApiClient (MealSession)dto.MealSession, dto.EmployeeSiteId, dto.CurrentSiteId); + + FileLogger.Info( + "ClientApi", + $"API scan request completed. Success={scanResult.Success}, Message={scanResult.Message}"); + return scanResult; } catch (Exception ex) { + FileLogger.Error("ClientApi", "API scan request failed with exception.", ex); Logger.Log(ex, "CanteenBackendApiClient.ProcessScanDetailed"); return new ScanResult(false, "Cannot reach backend: " + ex.Message, 0); } @@ -172,29 +197,40 @@ public class CanteenBackendApiClient : ICanteenBackendApiClient }; } + var url = $"{BaseUrl}{path}"; + FileLogger.Info("ClientApi", $"POST {url} started."); + try { - var url = $"{BaseUrl}{path}"; using var response = await _http.PostAsync(url, null, cancellationToken).ConfigureAwait(false); var dto = await response.Content.ReadFromJsonAsync(JsonOptions, cancellationToken) .ConfigureAwait(false); if (dto != null) + { + FileLogger.Info( + "ClientApi", + $"POST {path} completed. Success={dto.Success}, Message={dto.Message}, Details={dto.Details}"); return dto; + } if (!response.IsSuccessStatusCode) { var err = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); - return new ManualSyncResponse + var fail = new ManualSyncResponse { Success = false, Message = $"Backend error ({(int)response.StatusCode}): {Truncate(err, 200)}" }; + FileLogger.Warn("ClientApi", $"POST {path} failed. {fail.Message}"); + return fail; } + FileLogger.Warn("ClientApi", $"POST {path} failed. Invalid response from backend."); return new ManualSyncResponse { Success = false, Message = "Invalid response from backend." }; } catch (Exception ex) { + FileLogger.Error("ClientApi", $"POST {path} failed with exception.", ex); Logger.Log(ex, $"CanteenBackendApiClient.PostJobAsync{path}"); return new ManualSyncResponse { diff --git a/Services/ISyncService.cs b/Services/ISyncService.cs index 970dd60..92c0697 100644 --- a/Services/ISyncService.cs +++ b/Services/ISyncService.cs @@ -5,6 +5,6 @@ namespace UtopiaCanteenSystem.Services; /// public interface ISyncService { - /// Runs one sync: POST unsynced records to API and mark as synced on success. - Task SyncNowAsync(CancellationToken cancellationToken = default); + /// Runs one sync: POST unsynced records to production/HRMS and mark as synced on success. + Task SyncNowAsync(CancellationToken cancellationToken = default); } diff --git a/Services/SyncNowResult.cs b/Services/SyncNowResult.cs new file mode 100644 index 0000000..357c2af --- /dev/null +++ b/Services/SyncNowResult.cs @@ -0,0 +1,10 @@ +namespace UtopiaCanteenSystem.Services; + +public sealed class SyncNowResult +{ + public int PendingCount { get; init; } + public int PostedCount { get; init; } + public int DuplicatesSkippedCount { get; init; } + public int FailedCount { get; init; } + public bool SkippedNoConnection { get; init; } +} diff --git a/Services/SyncService.cs b/Services/SyncService.cs index 36c5dc0..b926ffc 100644 --- a/Services/SyncService.cs +++ b/Services/SyncService.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using MySqlConnector; using UtopiaCanteenSystem.Data; using UtopiaCanteenSystem.Models; +using UtopiaCanteenSystem.Services.Logging; namespace UtopiaCanteenSystem.Services; @@ -20,13 +21,13 @@ public class SyncService : ISyncService _configService = configService; } - public async Task SyncNowAsync(CancellationToken cancellationToken = default) + public async Task SyncNowAsync(CancellationToken cancellationToken = default) { var productionConnStr = _configService.GetMySqlConnectionString(); if (string.IsNullOrWhiteSpace(productionConnStr)) { - System.Diagnostics.Debug.WriteLine("MySQL connection string (production) not configured; skipping sync."); - return; + FileLogger.Warn("OrderSync", "MySQL connection string is not configured; skipping order sync."); + return new SyncNowResult { SkippedNoConnection = true }; } var hrmsConnStr = _configService.GetHrmsLookupConnectionString(); @@ -41,11 +42,20 @@ public class SyncService : ISyncService .ConfigureAwait(false); } + FileLogger.Info("OrderSync", $"Order sync started. PendingCount={toSync.Count}"); + + var posted = 0; + var duplicatesSkipped = 0; + var failed = 0; + // Always run day-end cleanup (remove synced rows from previous days), even when there's nothing to sync. await CleanupOldSyncedRowsAsync(cancellationToken).ConfigureAwait(false); if (toSync.Count == 0) - return; + { + FileLogger.Info("OrderSync", "Order sync completed. No pending orders."); + return new SyncNowResult(); + } // Production: lunch_order_transactions (GetMySqlConnectionString) const string insertTxnSql = @" @@ -135,6 +145,7 @@ public class SyncService : ISyncService if (await IsDuplicateMealInProductionAsync(record, hrmsConnStr, cancellationToken).ConfigureAwait(false)) { syncedIds.Add(record.Id); + duplicatesSkipped++; continue; } @@ -274,11 +285,12 @@ public class SyncService : ISyncService } syncedIds.Add(record.Id); + posted++; } catch (Exception ex) { - System.Diagnostics.Debug.WriteLine($"Sync record failed (SQLite Id={record.Id}): {ex.Message}"); - // Leave unsynced; will retry later. + failed++; + FileLogger.Error("OrderSync", $"UIND/HRMS post failed for SQLite Id={record.Id}, CardId={LogMasking.MaskCardId(record.CardId)}.", ex); } } @@ -300,8 +312,21 @@ public class SyncService : ISyncService } catch (Exception ex) { - System.Diagnostics.Debug.WriteLine($"Sync cleanup failed: {ex.Message}"); + FileLogger.Warn("OrderSync", "Sync cleanup failed.", ex); } + + var result = new SyncNowResult + { + PendingCount = toSync.Count, + PostedCount = posted, + DuplicatesSkippedCount = duplicatesSkipped, + FailedCount = failed + }; + FileLogger.Info( + "OrderSync", + $"Order sync completed. Pending={result.PendingCount}, Posted={result.PostedCount}, " + + $"DuplicatesSkipped={result.DuplicatesSkippedCount}, Failed={result.FailedCount}."); + return result; } private static string GenerateLunchOrderCode(long id, DateTime date)