using System.Net; using System.Net.Http.Json; using System.Text.Json; using FluentAssertions; using foodmarket.Api.Infrastructure.Invoices; using foodmarket.Domain.Purchases; using foodmarket.Infrastructure.Persistence; using foodmarket.IntegrationTests.Support; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Xunit; namespace foodmarket.IntegrationTests; /// Sprint 31 — приёмка по фото. Проверяем pipeline без реального /// Claude CLI: подставляем парсированный /// напрямую через InvoiceProcessor.BuildMatcherAsync, т.е. тестим /// matcher-on-real-catalog + alias upsert + apply → Supply Draft. [Collection(ApiCollection.Name)] public class InvoicePipelineTests { private readonly ApiFactory _factory; public InvoicePipelineTests(ApiFactory factory) => _factory = factory; [Fact] public async Task Matcher_hits_product_by_barcode_from_real_catalog() { using var scope = _factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); // Берём любую org из seed'ов (Demo Market или первую не-архивную) var demoOrg = await db.Organizations.IgnoreQueryFilters() .FirstAsync(o => !o.IsArchived && o.Name == "Demo Market"); // Нам нужен товар + штрихкод в этой org. Если нет — создадим. var (productId, barcode) = await EnsureProductWithBarcodeAsync(db, demoOrg.Id); var matcher = await InvoiceProcessor.BuildMatcherAsync(db, demoOrg.Id, "ТОО Поставщик", CancellationToken.None); var line = new InvoiceLine { Barcode = barcode, Name = "Что угодно" }; matcher.Match(line, "тоо поставщик"); line.MatchedBy.Should().Be("barcode"); line.ProductId.Should().Be(productId); } [Fact] public async Task Multi_tenant_isolation_matcher_does_not_see_other_orgs_barcodes() { using var scope = _factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var orgA = await db.Organizations.IgnoreQueryFilters() .FirstAsync(o => !o.IsArchived && o.Name == "Demo Market"); var orgB = await db.Organizations.IgnoreQueryFilters() .Where(o => !o.IsArchived && o.Id != orgA.Id).FirstOrDefaultAsync(); if (orgB is null) { // создадим вторую org для теста изоляции orgB = new foodmarket.Domain.Organizations.Organization { Name = $"InvOrgB-{Guid.NewGuid():N}", CountryCode = "KZ", Bin = "999999999999", }; db.Organizations.Add(orgB); await db.SaveChangesAsync(); } var (_, bcA) = await EnsureProductWithBarcodeAsync(db, orgA.Id); // Matcher для orgB — не должен находить barcode из orgA. var matcher = await InvoiceProcessor.BuildMatcherAsync(db, orgB.Id, null, CancellationToken.None); var line = new InvoiceLine { Barcode = bcA, Name = "X" }; matcher.Match(line, "any"); line.MatchedBy.Should().Be("none"); } private static async Task<(Guid productId, string barcode)> EnsureProductWithBarcodeAsync( AppDbContext db, Guid orgId) { var existing = await (from pb in db.ProductBarcodes.IgnoreQueryFilters() where pb.OrganizationId == orgId select new { pb.ProductId, pb.Code }).FirstOrDefaultAsync(); if (existing is not null) return (existing.ProductId, existing.Code); // Создаём минимальный товар + штрихкод (идемпотентный barcode). var unit = await db.UnitsOfMeasure.IgnoreQueryFilters() .FirstAsync(u => u.OrganizationId == null && u.Code == "796"); var group = await db.ProductGroups.IgnoreQueryFilters() .FirstAsync(g => g.OrganizationId == orgId && g.IsSystem); var p = new foodmarket.Domain.Catalog.Product { OrganizationId = orgId, Name = $"ТестТовар-{Guid.NewGuid():N}", UnitOfMeasureId = unit.Id, ProductGroupId = group.Id, Vat = 12m, }; db.Products.Add(p); var bc = $"4870{Random.Shared.Next(100000000, 999999999)}"; db.ProductBarcodes.Add(new foodmarket.Domain.Catalog.ProductBarcode { OrganizationId = orgId, ProductId = p.Id, Code = bc, Type = foodmarket.Domain.Catalog.BarcodeType.Ean13, IsPrimary = true, }); await db.SaveChangesAsync(); return (p.Id, bc); } }