From 3df4bc26726462d52020f004bdef6a9f60fb27a1 Mon Sep 17 00:00:00 2001 From: Syed Mustafa Ahmed Naqvi Date: Mon, 1 Jun 2026 15:14:26 +0500 Subject: [PATCH] Return order sync & Update sync contract and client API logging Changes order sync to return pending, posted, duplicate, failed, and skipped counts. Updates the sync interface for result reporting and logs client API health, scan, and manual sync calls. --- Services/CanteenBackendApiClient.cs | 46 +++++++++++++++++++++++++---- Services/ISyncService.cs | 4 +-- Services/SyncNowResult.cs | 10 +++++++ Services/SyncService.cs | 39 +++++++++++++++++++----- 4 files changed, 85 insertions(+), 14 deletions(-) create mode 100644 Services/SyncNowResult.cs 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)