< Summary

Information
Class: Services.AiAssistant.AiProductContextLoader
Assembly: Services
File(s): /home/runner/work/Studio2Prod/Studio2Prod/AppRepository/AiAssistant/AiProductContextLoader.cs
Line coverage
0%
Covered lines: 0
Uncovered lines: 81
Coverable lines: 81
Total lines: 147
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 38
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%210%
LoadFormattedAsync()0%2040%
AppendWarehouseStockSectionAsync()0%210140%
AppendMovementMapSectionAsync()0%420200%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/AppRepository/AiAssistant/AiProductContextLoader.cs

#LineLine coverage
 1using System.Text;
 2using Microsoft.EntityFrameworkCore;
 3using Microsoft.Extensions.Logging;
 4using OzonRepositories.Context;
 5using Services.ApiServices;
 6using Services.ApiServices._1CApi.Models;
 7using Services.StockSyncServices;
 8
 9namespace Services.AiAssistant;
 10
 11/// <summary>
 12/// Таблица остатков + карта движений 1С (как /WarehouseStock и movement-map).
 13/// </summary>
 14public sealed class AiProductContextLoader
 15{
 16    private readonly ApplicationDbContext _db;
 17    private readonly IWarehouseStockLookupService _warehouseLookup;
 18    private readonly OneCDataManager _oneCDataManager;
 19    private readonly ILogger<AiProductContextLoader> _logger;
 20
 021    public AiProductContextLoader(
 022        ApplicationDbContext db,
 023        IWarehouseStockLookupService warehouseLookup,
 024        OneCDataManager oneCDataManager,
 025        ILogger<AiProductContextLoader> logger)
 26    {
 027        _db                 = db;
 028        _warehouseLookup    = warehouseLookup;
 029        _oneCDataManager    = oneCDataManager;
 030        _logger             = logger;
 031    }
 32
 33    public async Task<string> LoadFormattedAsync(
 34        string article,
 35        string? manufacturer,
 36        CancellationToken ct = default)
 37    {
 038        article = article.Trim();
 039        if (string.IsNullOrWhiteSpace(article))
 040            return "Артикул товара не указан.";
 41
 042        var sb = new StringBuilder();
 043        sb.AppendLine($"=== Товар и склад (таблица остатков + 1С) — артикул {article} ===");
 044        if (!string.IsNullOrWhiteSpace(manufacturer))
 045            sb.AppendLine($"Производитель (из заказа/запроса): {manufacturer}");
 46
 047        await AppendWarehouseStockSectionAsync(sb, article, manufacturer, ct);
 048        await AppendMovementMapSectionAsync(sb, article, ct);
 49
 050        return sb.ToString().TrimEnd();
 051    }
 52
 53    private async Task AppendWarehouseStockSectionAsync(
 54        StringBuilder sb,
 55        string article,
 56        string? manufacturer,
 57        CancellationToken ct)
 58    {
 059        var lookup = await _warehouseLookup.LookupAsync(article, manufacturer, ct);
 060        var rows = await _db.WarehouseStockSnapshots
 061            .AsNoTracking()
 062            .Where(x => x.Article != null && x.Article.Contains(article))
 063            .OrderBy(x => x.Article)
 064            .Take(15)
 065            .ToListAsync(ct);
 66
 067        if (rows.Count == 0 && lookup is not { Found: true })
 68        {
 069            sb.AppendLine("Таблица остатков: строк не найдено.");
 070            return;
 71        }
 72
 073        sb.AppendLine("--- Таблица остатков ---");
 074        sb.AppendLine(
 075            "⚠ Дробные значения здесь — из Google Sheets/снэпшота. Отчёт BitrixStock и накат Ozon используют " +
 076            "целые штуки (Math.Round при построении отчёта). Не смешивай с OneCQty из синхронизации.");
 077        if (lookup is { Found: true })
 78        {
 079            sb.AppendLine(
 080                $"Лучшее совпадение: строка #{lookup.SourceRowNumber}, C0 {lookup.C0:N0} ₽, " +
 081                $"M2 {lookup.M2Qty} шт., M3 {lookup.M3Qty} шт., B1 {lookup.B1Qty} шт., B2 {lookup.B2Qty} шт." +
 082                (lookup.FuzzyManufacturer ? " (производитель — fuzzy match)" : ""));
 83        }
 84
 085        foreach (var r in rows)
 86        {
 087            sb.AppendLine(
 088                $"  • {r.Article} | {r.Manufacturer} | {r.ProductName} | " +
 089                $"M2 {r.M2Qty} шт., M3 {r.M3Qty} шт., B1 {r.B1Qty} шт., B2 {r.B2Qty} шт. | " +
 090                $"C0 {r.C0:N0} ₽ | Sale={r.SaleFlag} | sync {r.SyncedAtUtc:dd.MM.yyyy HH:mm} UTC");
 91        }
 092    }
 93
 94    private async Task AppendMovementMapSectionAsync(StringBuilder sb, string article, CancellationToken ct)
 95    {
 096        sb.AppendLine();
 097        sb.AppendLine("--- Карта движений 1С ---");
 98
 99        try
 100        {
 0101            var map = await _oneCDataManager.GetMovementMapAsync(article);
 0102            if (map == null)
 103            {
 0104                sb.AppendLine("В 1С номенклатура по этому артикулу не найдена.");
 0105                return;
 106            }
 107
 0108            sb.AppendLine($"Наименование 1С: {map.NomenclatureName}");
 0109            if (map.CurrentBalance is { Count: > 0 })
 110            {
 0111                sb.AppendLine("Текущий остаток по складам 1С:");
 0112                foreach (var b in map.CurrentBalance)
 0113                    sb.AppendLine($"  • {b.Warehouse}: {b.Quantity} шт.");
 114            }
 115
 0116            sb.AppendLine(
 0117                $"Итого приход: {map.TotalReceiptQty} шт. ({map.TotalReceiptDocs} док.), " +
 0118                $"расход: {map.TotalExpenseQty} ({map.TotalExpenseDocs}), " +
 0119                $"перемещения: {map.TotalTransferQty} ({map.TotalTransferDocs})");
 120
 0121            var movements = (map.Movements ?? [])
 0122                .Where(m => !m.IsDeleted)
 0123                .OrderByDescending(m => m.Date)
 0124                .Take(12)
 0125                .ToList();
 126
 0127            if (movements.Count > 0)
 128            {
 0129                sb.AppendLine("Последние движения:");
 0130                foreach (var m in movements)
 131                {
 0132                    var wh = m.Kind == MovementKind.Transfer
 0133                        ? $"{m.WarehouseFrom} → {m.WarehouseTo}"
 0134                        : m.WarehouseFrom ?? m.WarehouseTo ?? "—";
 0135                    sb.AppendLine(
 0136                        $"  {m.Date:dd.MM.yyyy HH:mm} | {m.Kind} | {m.DocType} №{m.DocNumber} | " +
 0137                        $"{m.Quantity} шт. | {m.Price:N0} ₽ | {wh}");
 138                }
 139            }
 0140        }
 0141        catch (Exception ex)
 142        {
 0143            _logger.LogWarning(ex, "AI product context: movement-map failed for {Article}", article);
 0144            sb.AppendLine($"Карта движений 1С временно недоступна: {ex.Message}");
 0145        }
 0146    }
 147}