using System.Text.Json; using Meezi.Core.Entities; using Meezi.Core.Interfaces; using Meezi.Infrastructure.Data; using Microsoft.Extensions.DependencyInjection; namespace Meezi.API.Services; /// /// Persists audit entries on a fresh, isolated so the /// write never participates in (or rolls back with) the caller's transaction, and /// swallows all failures — auditing must never break the recorded operation. /// public sealed class AuditLogService : IAuditLogService { private static readonly JsonSerializerOptions DetailsJsonOptions = new() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase, DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull }; private readonly ITenantContext _tenant; private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger _logger; public AuditLogService( ITenantContext tenant, IServiceScopeFactory scopeFactory, ILogger logger) { _tenant = tenant; _scopeFactory = scopeFactory; _logger = logger; } public async Task LogAsync(AuditEntry entry, CancellationToken ct = default) { try { var cafeId = _tenant.CafeId; if (string.IsNullOrEmpty(cafeId)) { _logger.LogWarning( "Skipping audit log '{Category}/{Action}' — no cafe context.", entry.Category, entry.Action); return; } var log = new AuditLog { CafeId = cafeId, BranchId = entry.BranchId ?? _tenant.BranchId, Category = entry.Category, Action = entry.Action, EntityType = entry.EntityType, EntityId = entry.EntityId, ActorId = _tenant.UserId, ActorName = entry.ActorName, ActorRole = _tenant.Role?.ToString(), Summary = entry.Summary, DetailsJson = entry.Details is null ? null : JsonSerializer.Serialize(entry.Details, DetailsJsonOptions) }; // Fresh scope → fresh DbContext (café-wide, unfiltered) so this write is // independent of the business operation's change-tracker and transaction. using var scope = _scopeFactory.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); db.AuditLogs.Add(log); await db.SaveChangesAsync(ct); } catch (Exception ex) { _logger.LogError(ex, "Failed to write audit log '{Category}/{Action}' for entity {EntityType}:{EntityId}.", entry.Category, entry.Action, entry.EntityType, entry.EntityId); } } }