fix(inventory): записывать автора движения (CauseUserId) для всех документов

CauseUserId в уведомлениях о минусах был null: StockService писал автора только
если вызывающий явно передал CreatedByUserId в черновике, а большинство
документов (касса/списание/перемещение/приёмка/…) его не передавали. Теперь
автор берётся из черновика, иначе — из текущего пользователя (ITenantContext.UserId)
в единой точке ApplyMovementAsync, поэтому пишется и в StockMovement, и в алерт
для всех типов документов. Тест: минус от списания → causeUserId = текущий юзер.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-09 09:52:33 +05:00
parent 4aceda3e41
commit 9dc8f6e044
2 changed files with 12 additions and 2 deletions

View file

@ -22,6 +22,10 @@ public StockService(AppDbContext db, ITenantContext tenant, INegativeStockAlertS
public async Task<decimal> ApplyMovementAsync(StockMovementDraft d, CancellationToken ct = default) public async Task<decimal> ApplyMovementAsync(StockMovementDraft d, CancellationToken ct = default)
{ {
var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("Tenant not set."); var orgId = _tenant.OrganizationId ?? throw new InvalidOperationException("Tenant not set.");
// Автор движения: берём из черновика, иначе — текущий пользователь (JWT). Так
// автор пишется для ВСЕХ документов (касса/списание/перемещение/приёмка/…),
// даже если вызывающий его явно не указал — иначе CauseUserId в алертах был null.
var userId = d.CreatedByUserId ?? _tenant.UserId;
_db.StockMovements.Add(new StockMovement _db.StockMovements.Add(new StockMovement
{ {
@ -35,7 +39,7 @@ public async Task<decimal> ApplyMovementAsync(StockMovementDraft d, Cancellation
DocumentId = d.DocumentId, DocumentId = d.DocumentId,
DocumentNumber = d.DocumentNumber, DocumentNumber = d.DocumentNumber,
OccurredAt = d.OccurredAt ?? DateTime.UtcNow, OccurredAt = d.OccurredAt ?? DateTime.UtcNow,
CreatedByUserId = d.CreatedByUserId, CreatedByUserId = userId,
Notes = d.Notes, Notes = d.Notes,
}); });
@ -61,7 +65,7 @@ public async Task<decimal> ApplyMovementAsync(StockMovementDraft d, Cancellation
// Предупреждение об отрицательном остатке: единая точка детекции для всех // Предупреждение об отрицательном остатке: единая точка детекции для всех
// документов. Остаток < 0 → открыть/обновить алерт; ≥ 0 → закрыть. // документов. Остаток < 0 → открыть/обновить алерт; ≥ 0 → закрыть.
await _negAlerts.OnStockLevelAsync(d.StoreId, d.ProductId, stock.Quantity, await _negAlerts.OnStockLevelAsync(d.StoreId, d.ProductId, stock.Quantity,
d.DocumentType, d.DocumentId, d.DocumentNumber, d.CreatedByUserId, d.OccurredAt, ct); d.DocumentType, d.DocumentId, d.DocumentNumber, userId, d.OccurredAt, ct);
return stock.Quantity; return stock.Quantity;
} }

View file

@ -64,6 +64,12 @@ public async Task Loss_into_negative_is_allowed_creates_alert_recount_and_resolv
alerts[0].GetProperty("quantity").GetDecimal().Should().Be(-3m); alerts[0].GetProperty("quantity").GetDecimal().Should().Be(-3m);
alerts[0].GetProperty("productId").GetString().Should().Be(p); alerts[0].GetProperty("productId").GetString().Should().Be(p);
alerts[0].GetProperty("causeDocType").GetString().Should().Be("loss"); alerts[0].GetProperty("causeDocType").GetString().Should().Be("loss");
// Автор документа записан (раньше CauseUserId был null). Сверяем с /api/me.
var me = await api.GetJsonAsync("/api/me");
var myId = me.GetProperty("sub").GetString();
alerts[0].TryGetProperty("causeUserId", out var cu).Should().BeTrue();
cu.GetString().Should().NotBeNullOrEmpty("автор минусового документа должен быть записан");
cu.GetString().Should().Be(myId, "автор — текущий пользователь");
// «Пересчитать эти товары» → черновик инвентаризации с этим товаром. // «Пересчитать эти товары» → черновик инвентаризации с этим товаром.
var rc = await api.Http.PostAsJsonAsync("/api/negative-stock/recount", new { storeId = refs.StoreId }); var rc = await api.Http.PostAsJsonAsync("/api/negative-stock/recount", new { storeId = refs.StoreId });