Показывать в аудите изменение «было → стало»
Перемещение карточки пишет прежнюю и новую колонку (CardMoveResultDto.From/To), изменение колонки — переименование «было → стало», разбор старого плоского формата спаривает oldBudget/budgetTokens и oldPeriod/period. Добавлены тесты разбора.
This commit is contained in:
@@ -182,7 +182,14 @@ public static class CardsEndpoints
|
||||
return EndpointResults.BadRequest(outcome.Error);
|
||||
}
|
||||
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.CardMoved, [AuditDetails.Set(AuditFields.CardId, cardId), AuditDetails.Set(AuditFields.Destination, body.To)], ct);
|
||||
await AuditAppender.AppendTenantAsync(
|
||||
context,
|
||||
AuditEvents.CardMoved,
|
||||
[
|
||||
AuditDetails.Set(AuditFields.CardId, cardId),
|
||||
AuditDetails.Change(AuditFields.ContainerId, outcome.From, outcome.To),
|
||||
],
|
||||
ct);
|
||||
|
||||
CardsService cardsService = context.RequestServices.GetRequiredService<CardsService>();
|
||||
CardDto unified = await cardsService.GetCardAsync(cardId, ct);
|
||||
|
||||
@@ -140,6 +140,7 @@ public static class ContainersEndpoints
|
||||
}
|
||||
|
||||
ContainersService containers = context.RequestServices.GetRequiredService<ContainersService>();
|
||||
ContainerDto before = await containers.GetAsync(containerId, ct);
|
||||
ContainerDto updated = await containers.PatchAsync(
|
||||
containerId,
|
||||
new ContainerPatchDto(
|
||||
@@ -153,7 +154,13 @@ public static class ContainersEndpoints
|
||||
patchBody.Policy),
|
||||
ct);
|
||||
|
||||
await AuditAppender.AppendTenantAsync(context, AuditEvents.ContainerUpdated, [AuditDetails.Set(AuditFields.ContainerId, updated.Id)], ct);
|
||||
await AuditAppender.AppendTenantAsync(
|
||||
context,
|
||||
AuditEvents.ContainerUpdated,
|
||||
string.Equals(before.Name, updated.Name, StringComparison.Ordinal)
|
||||
? [AuditDetails.Set(AuditFields.ContainerId, updated.Id)]
|
||||
: [AuditDetails.Change(AuditFields.Name, before.Name, updated.Name)],
|
||||
ct);
|
||||
return Results.Ok(new { id = updated.Id });
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,6 @@ public sealed class CardMover(CardsService cardsService) : ICardMover
|
||||
CardResultDto result = CardsDefaultContainers.Contains(toContainerId)
|
||||
? await cardsService.MoveStageCardAsync(cardId, toContainerId, ct)
|
||||
: await cardsService.MoveDashboardCardAsync(cardId, toContainerId, ct);
|
||||
return new CardMoveResultDto(result.Error);
|
||||
return new CardMoveResultDto(result.Error, From: result.Card?.PrevCol, To: result.Card?.Col);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,6 @@ namespace Deal.Modules.Cards.Application.Dtos;
|
||||
/// Результат перехода карточки единым механизмом <see cref="ICardMover"/>.
|
||||
/// </summary>
|
||||
/// <param name="Error">Текст 400-ошибки либо null (успех).</param>
|
||||
public sealed record CardMoveResultDto(string? Error);
|
||||
/// <param name="From">Контейнер-источник после перехода; null — переход не выполнен.</param>
|
||||
/// <param name="To">Контейнер-назначение после перехода; null — переход не выполнен.</param>
|
||||
public sealed record CardMoveResultDto(string? Error, string? From = null, string? To = null);
|
||||
|
||||
@@ -22,6 +22,14 @@ public static class AuditRecordDtoExtensions
|
||||
private const string OldPrefix = "old";
|
||||
private const string NewPrefix = "new";
|
||||
|
||||
// Соответствие суффикса «old<X>» имени ключа нового значения в устаревшем формате (имена не совпадают).
|
||||
private static readonly IReadOnlyDictionary<string, string> LegacyNewNameByOldSuffix =
|
||||
new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["Budget"] = "budgetTokens",
|
||||
["Period"] = "period",
|
||||
};
|
||||
|
||||
// События аудита «неудачный вход» (тенант/оператор).
|
||||
private static readonly string[] FailedLoginEvents =
|
||||
{
|
||||
@@ -140,10 +148,23 @@ public static class AuditRecordDtoExtensions
|
||||
continue;
|
||||
}
|
||||
|
||||
string? pairedNewName = property.Name.StartsWith(OldPrefix, StringComparison.Ordinal)
|
||||
&& property.Name.Length > OldPrefix.Length
|
||||
? NewPrefix + property.Name[OldPrefix.Length..]
|
||||
: null;
|
||||
string? pairedNewName = null;
|
||||
if (property.Name.StartsWith(OldPrefix, StringComparison.Ordinal)
|
||||
&& property.Name.Length > OldPrefix.Length)
|
||||
{
|
||||
string suffix = property.Name[OldPrefix.Length..];
|
||||
string defaultNewName = NewPrefix + suffix;
|
||||
if (LegacyNewNameByOldSuffix.TryGetValue(suffix, out string? mapped)
|
||||
&& root.TryGetProperty(mapped, out JsonElement mappedValue))
|
||||
{
|
||||
consumed.Add(mapped);
|
||||
result.Add(new AuditChangeDto(LowerFirst(suffix), Stringify(property.Value), Stringify(mappedValue)));
|
||||
continue;
|
||||
}
|
||||
|
||||
pairedNewName = root.TryGetProperty(defaultNewName, out _) ? defaultNewName : null;
|
||||
}
|
||||
|
||||
if (pairedNewName is not null && root.TryGetProperty(pairedNewName, out JsonElement newValue))
|
||||
{
|
||||
consumed.Add(pairedNewName);
|
||||
|
||||
@@ -57,6 +57,30 @@ public sealed class AuditRecordDtoExtensionsTests
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AuditChanges_LegacyLimitChange_PairsBudgetAndPeriod()
|
||||
{
|
||||
var record = Record(
|
||||
"""{"tenantId":"6f3c0d1e-2b4a-4c8d-9e0f-1a2b3c4d5e6f","oldBudget":100000,"oldPeriod":"month","budgetTokens":1000000,"period":"month"}""");
|
||||
|
||||
IReadOnlyList<AuditChangeDto> changes = record.AuditChanges();
|
||||
|
||||
Assert.Collection(
|
||||
changes,
|
||||
budget =>
|
||||
{
|
||||
Assert.Equal("budget", budget.Field);
|
||||
Assert.Equal("100000", budget.From);
|
||||
Assert.Equal("1000000", budget.To);
|
||||
},
|
||||
period =>
|
||||
{
|
||||
Assert.Equal("period", period.Field);
|
||||
Assert.Equal("month", period.From);
|
||||
Assert.Equal("month", period.To);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AuditChanges_InvalidJson_ReturnsEmpty()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user