| | | 1 | | using Microsoft.EntityFrameworkCore; |
| | | 2 | | using OzonRepositories.Context; |
| | | 3 | | using Services.ApiServices; |
| | | 4 | | using Services.BitrixStockServices; |
| | | 5 | | using Services.StockSyncServices; |
| | | 6 | | |
| | | 7 | | namespace Services.AiAssistant; |
| | | 8 | | |
| | | 9 | | public sealed class AiChatEnrichmentBuilder |
| | | 10 | | { |
| | | 11 | | private readonly IBitrixStockSyncArticleHistoryService _bitrixHistory; |
| | | 12 | | private readonly ApplicationDbContext _db; |
| | | 13 | | private readonly IWarehouseStockLookupService _warehouseLookup; |
| | | 14 | | private readonly OneCDataManager _oneCDataManager; |
| | | 15 | | |
| | 19 | 16 | | public AiChatEnrichmentBuilder( |
| | 19 | 17 | | IBitrixStockSyncArticleHistoryService bitrixHistory, |
| | 19 | 18 | | ApplicationDbContext db, |
| | 19 | 19 | | IWarehouseStockLookupService warehouseLookup, |
| | 19 | 20 | | OneCDataManager oneCDataManager) |
| | | 21 | | { |
| | 19 | 22 | | _bitrixHistory = bitrixHistory; |
| | 19 | 23 | | _db = db; |
| | 19 | 24 | | _warehouseLookup = warehouseLookup; |
| | 19 | 25 | | _oneCDataManager = oneCDataManager; |
| | 19 | 26 | | } |
| | | 27 | | |
| | | 28 | | public async Task<AiChatEnrichmentDto?> BuildForArticleAsync( |
| | | 29 | | string article, |
| | | 30 | | string? manufacturer, |
| | | 31 | | string? productImageUrl = null, |
| | | 32 | | string? productName = null, |
| | | 33 | | CancellationToken ct = default) |
| | | 34 | | { |
| | 19 | 35 | | article = article.Trim(); |
| | 18 | 36 | | if (string.IsNullOrWhiteSpace(article)) |
| | 3 | 37 | | return null; |
| | | 38 | | |
| | 15 | 39 | | var page = await _bitrixHistory.GetAllAsync(article, maxReports: 15, ct); |
| | 15 | 40 | | var visual = await ResolveProductVisualAsync(article, productImageUrl, productName, ct); |
| | | 41 | | |
| | 15 | 42 | | return new AiChatEnrichmentDto |
| | 15 | 43 | | { |
| | 15 | 44 | | Article = article, |
| | 15 | 45 | | ProductImageUrl = visual.ImageUrl, |
| | 15 | 46 | | ProductName = visual.ProductName, |
| | 15 | 47 | | BitrixSync = BitrixStockSyncUiMapper.Map(page), |
| | 15 | 48 | | StockTable = await BuildStockTableAsync(article, manufacturer, ct), |
| | 15 | 49 | | MovementMap = await BuildMovementMapAsync(article, ct) |
| | 15 | 50 | | }; |
| | 18 | 51 | | } |
| | | 52 | | |
| | | 53 | | private async Task<(string? ImageUrl, string? ProductName)> ResolveProductVisualAsync( |
| | | 54 | | string article, |
| | | 55 | | string? imageUrl, |
| | | 56 | | string? productName, |
| | | 57 | | CancellationToken ct) |
| | | 58 | | { |
| | 15 | 59 | | if (!string.IsNullOrWhiteSpace(imageUrl)) |
| | 1 | 60 | | return (imageUrl.Trim(), productName); |
| | | 61 | | |
| | 14 | 62 | | var fromProduct = await _db.Products |
| | 14 | 63 | | .AsNoTracking() |
| | 14 | 64 | | .Where(p => p.Article == article && p.ImageUrl != null && p.ImageUrl != "") |
| | 14 | 65 | | .OrderByDescending(p => p.Id) |
| | 14 | 66 | | .Select(p => p.ImageUrl) |
| | 14 | 67 | | .FirstOrDefaultAsync(ct); |
| | | 68 | | |
| | 14 | 69 | | if (!string.IsNullOrWhiteSpace(fromProduct)) |
| | | 70 | | { |
| | 3 | 71 | | productName ??= await _db.WarehouseStockSnapshots |
| | 3 | 72 | | .AsNoTracking() |
| | 3 | 73 | | .Where(x => x.Article == article && x.ProductName != null && x.ProductName != "") |
| | 3 | 74 | | .Select(x => x.ProductName) |
| | 3 | 75 | | .FirstOrDefaultAsync(ct); |
| | 3 | 76 | | return (fromProduct, productName); |
| | | 77 | | } |
| | | 78 | | |
| | 11 | 79 | | return (null, productName); |
| | 15 | 80 | | } |
| | | 81 | | |
| | | 82 | | private async Task<AiWarehouseStockUiDto?> BuildStockTableAsync( |
| | | 83 | | string article, |
| | | 84 | | string? manufacturer, |
| | | 85 | | CancellationToken ct) |
| | | 86 | | { |
| | 15 | 87 | | var lookup = await _warehouseLookup.LookupAsync(article, manufacturer, ct); |
| | 15 | 88 | | var rows = await _db.WarehouseStockSnapshots |
| | 15 | 89 | | .AsNoTracking() |
| | 15 | 90 | | .Where(x => x.Article != null && x.Article.Contains(article)) |
| | 15 | 91 | | .OrderBy(x => x.Article) |
| | 15 | 92 | | .Take(12) |
| | 15 | 93 | | .ToListAsync(ct); |
| | | 94 | | |
| | 15 | 95 | | if (rows.Count == 0) |
| | | 96 | | { |
| | 11 | 97 | | if (lookup is not { Found: true }) |
| | 10 | 98 | | return null; |
| | | 99 | | |
| | 1 | 100 | | return new AiWarehouseStockUiDto |
| | 1 | 101 | | { |
| | 1 | 102 | | Article = article, |
| | 1 | 103 | | Rows = |
| | 1 | 104 | | [ |
| | 1 | 105 | | new AiWarehouseStockRowUiDto |
| | 1 | 106 | | { |
| | 1 | 107 | | Article = article, |
| | 1 | 108 | | C0 = lookup.C0, |
| | 1 | 109 | | M2Qty = lookup.M2Qty, |
| | 1 | 110 | | M3Qty = lookup.M3Qty, |
| | 1 | 111 | | B1Qty = lookup.B1Qty, |
| | 1 | 112 | | B2Qty = lookup.B2Qty |
| | 1 | 113 | | } |
| | 1 | 114 | | ] |
| | 1 | 115 | | }; |
| | | 116 | | } |
| | | 117 | | |
| | 4 | 118 | | return new AiWarehouseStockUiDto |
| | 4 | 119 | | { |
| | 4 | 120 | | Article = article, |
| | 15 | 121 | | Rows = rows.Select(r => new AiWarehouseStockRowUiDto |
| | 15 | 122 | | { |
| | 15 | 123 | | Article = r.Article ?? article, |
| | 15 | 124 | | Manufacturer = r.Manufacturer, |
| | 15 | 125 | | ProductName = r.ProductName, |
| | 15 | 126 | | M2Qty = r.M2Qty, |
| | 15 | 127 | | M3Qty = r.M3Qty, |
| | 15 | 128 | | B1Qty = r.B1Qty, |
| | 15 | 129 | | B2Qty = r.B2Qty, |
| | 15 | 130 | | C0 = r.C0, |
| | 15 | 131 | | SaleFlag = r.SaleFlag, |
| | 15 | 132 | | SyncedAtMsk = BitrixStockMskDateTime.Format(r.SyncedAtUtc) |
| | 15 | 133 | | }).ToList() |
| | 4 | 134 | | }; |
| | 15 | 135 | | } |
| | | 136 | | |
| | | 137 | | private async Task<AiMovementMapUiDto?> BuildMovementMapAsync(string article, CancellationToken ct) |
| | | 138 | | { |
| | | 139 | | try |
| | | 140 | | { |
| | 15 | 141 | | var map = await _oneCDataManager.GetMovementMapAsync(article); |
| | 14 | 142 | | if (map == null) |
| | 13 | 143 | | return null; |
| | | 144 | | |
| | 1 | 145 | | return new AiMovementMapUiDto |
| | 1 | 146 | | { |
| | 1 | 147 | | Article = map.Article ?? article, |
| | 1 | 148 | | NomenclatureName = map.NomenclatureName ?? article, |
| | 1 | 149 | | Balances = (map.CurrentBalance ?? []).Select(b => new AiMovementBalanceUiDto |
| | 1 | 150 | | { |
| | 1 | 151 | | Warehouse = b.Warehouse ?? "—", |
| | 1 | 152 | | Quantity = b.Quantity |
| | 1 | 153 | | }).ToList(), |
| | 1 | 154 | | Items = (map.Movements ?? []) |
| | 3 | 155 | | .Where(m => !m.IsDeleted) |
| | 2 | 156 | | .OrderByDescending(m => m.Date) |
| | 1 | 157 | | .Take(10) |
| | 2 | 158 | | .Select(m => new AiMovementItemUiDto |
| | 2 | 159 | | { |
| | 2 | 160 | | DateMsk = m.Date.ToString("dd.MM.yyyy HH:mm"), |
| | 2 | 161 | | Kind = m.Kind.ToString().ToLowerInvariant(), |
| | 2 | 162 | | DocType = m.DocType, |
| | 2 | 163 | | DocNumber = m.DocNumber, |
| | 2 | 164 | | Quantity = m.Quantity, |
| | 2 | 165 | | Price = m.Price, |
| | 2 | 166 | | WarehouseFrom = m.WarehouseFrom, |
| | 2 | 167 | | WarehouseTo = m.WarehouseTo |
| | 2 | 168 | | }).ToList() |
| | 1 | 169 | | }; |
| | | 170 | | } |
| | 1 | 171 | | catch |
| | | 172 | | { |
| | 1 | 173 | | return null; |
| | | 174 | | } |
| | 15 | 175 | | } |
| | | 176 | | } |