diff --git a/src/core/Deal.Api/Endpoints/CardsEndpoints.cs b/src/core/Deal.Api/Endpoints/CardsEndpoints.cs index 216139a..0b56d94 100644 --- a/src/core/Deal.Api/Endpoints/CardsEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/CardsEndpoints.cs @@ -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(); CardDto unified = await cardsService.GetCardAsync(cardId, ct); diff --git a/src/core/Deal.Api/Endpoints/ContainersEndpoints.cs b/src/core/Deal.Api/Endpoints/ContainersEndpoints.cs index 5bc70ec..4dc32ba 100644 --- a/src/core/Deal.Api/Endpoints/ContainersEndpoints.cs +++ b/src/core/Deal.Api/Endpoints/ContainersEndpoints.cs @@ -140,6 +140,7 @@ public static class ContainersEndpoints } ContainersService containers = context.RequestServices.GetRequiredService(); + 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 }); } diff --git a/src/core/Deal.Infrastructure/Services/CardMover.cs b/src/core/Deal.Infrastructure/Services/CardMover.cs index 1f59b4c..e7d5459 100644 --- a/src/core/Deal.Infrastructure/Services/CardMover.cs +++ b/src/core/Deal.Infrastructure/Services/CardMover.cs @@ -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); } } diff --git a/src/core/Deal.Modules.Cards/Application/Dtos/CardMoveResultDto.cs b/src/core/Deal.Modules.Cards/Application/Dtos/CardMoveResultDto.cs index 06bfa6a..65f7e01 100644 --- a/src/core/Deal.Modules.Cards/Application/Dtos/CardMoveResultDto.cs +++ b/src/core/Deal.Modules.Cards/Application/Dtos/CardMoveResultDto.cs @@ -6,4 +6,6 @@ namespace Deal.Modules.Cards.Application.Dtos; /// Результат перехода карточки единым механизмом . /// /// Текст 400-ошибки либо null (успех). -public sealed record CardMoveResultDto(string? Error); +/// Контейнер-источник после перехода; null — переход не выполнен. +/// Контейнер-назначение после перехода; null — переход не выполнен. +public sealed record CardMoveResultDto(string? Error, string? From = null, string? To = null); diff --git a/src/core/Deal.Modules.Tenants/Application/Extensions/AuditRecordDtoExtensions.cs b/src/core/Deal.Modules.Tenants/Application/Extensions/AuditRecordDtoExtensions.cs index 445bb56..b0879fd 100644 --- a/src/core/Deal.Modules.Tenants/Application/Extensions/AuditRecordDtoExtensions.cs +++ b/src/core/Deal.Modules.Tenants/Application/Extensions/AuditRecordDtoExtensions.cs @@ -22,6 +22,14 @@ public static class AuditRecordDtoExtensions private const string OldPrefix = "old"; private const string NewPrefix = "new"; + // Соответствие суффикса «old» имени ключа нового значения в устаревшем формате (имена не совпадают). + private static readonly IReadOnlyDictionary LegacyNewNameByOldSuffix = + new Dictionary(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); diff --git a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuditRecordDtoExtensionsTests.cs b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuditRecordDtoExtensionsTests.cs index eba20e8..f9bd186 100644 --- a/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuditRecordDtoExtensionsTests.cs +++ b/src/core/tests/Deal.Tests.Unit/Modules/Tenants/AuditRecordDtoExtensionsTests.cs @@ -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 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() {