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);
}
}
}