Unit: - InvoiceMatcherTests (12): barcode wins, short barcode falls through, alias by code/by name_norm, alias is supplier-scoped, по названию НЕ матчим автоматически (sklad-урок про LD Super Slims), NormalizeName quotes/whitespace/unit-glue/ё→е, OnlyDigits, AliasKeys skip пустые. - ClaudeCliInvoiceExtractorParseTests (8): plain JSON, envelope с result+usage, markdown fence, barcode spaces strip, null-tolerant, 'empty' raises, пустое name пропускается. Parse/FromJson теперь public (internal мешал тестам, InternalsVisibleTo избыточен). Integration: - Matcher_hits_product_by_barcode_from_real_catalog — реальный Postgres + seeded org + создаём товар+штрихкод, matcher находит. - Multi_tenant_isolation_matcher_does_not_see_other_orgs_barcodes — две org, штрихкод orgA НЕ виден в matcher'е orgB (tenant isolation). Все новые тесты зелёные. Pre-existing failures в full-suite не связаны. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
80 lines
3.2 KiB
C#
80 lines
3.2 KiB
C#
using FluentAssertions;
|
||
using foodmarket.Api.Infrastructure.Invoices;
|
||
using Xunit;
|
||
|
||
namespace foodmarket.UnitTests;
|
||
|
||
/// <summary>Проверка парсинга ответа Claude CLI. CLI отдаёт конверт
|
||
/// <c>{ result: "…json…", usage: {...} }</c>; result иногда обёрнут в
|
||
/// <c>```json</c>. Нужно вытащить payload и привести к ParsedInvoice.</summary>
|
||
public class ClaudeCliInvoiceExtractorParseTests
|
||
{
|
||
[Fact]
|
||
public void Parse_plain_json_payload()
|
||
{
|
||
var json = """{"supplier":"ТОО Фуд","number":"Н-1","date":"2026-10-04","total":1500.00,"lines":[{"barcode":"4870123456789","supplier_code":"ABC","name":"Молоко 3.2%","qty":10,"unit":"шт","price":150.00,"sum":1500.00,"vat":12}]}""";
|
||
var inv = ClaudeCliInvoiceExtractor.Parse(json);
|
||
inv.SupplierName.Should().Be("ТОО Фуд");
|
||
inv.InvoiceNumber.Should().Be("Н-1");
|
||
inv.TotalSum.Should().Be(1500m);
|
||
inv.Lines.Should().HaveCount(1);
|
||
var l = inv.Lines[0];
|
||
l.Barcode.Should().Be("4870123456789");
|
||
l.SupplierCode.Should().Be("ABC");
|
||
l.Quantity.Should().Be(10m);
|
||
l.Price.Should().Be(150m);
|
||
}
|
||
|
||
[Fact]
|
||
public void Parse_envelope_with_result_string_and_markdown_fence()
|
||
{
|
||
// Payload wrapped в ```json ``` — Claude иногда так возвращает.
|
||
var fence = "`" + "`" + "`";
|
||
var payloadInside = fence + "json\n{\"supplier\":\"X\",\"lines\":[{\"name\":\"А\",\"qty\":1,\"price\":10}]}\n" + fence;
|
||
var envelope = System.Text.Json.JsonSerializer.Serialize(new
|
||
{
|
||
result = payloadInside,
|
||
usage = new { input_tokens = 123, output_tokens = 45 }
|
||
});
|
||
var inv = ClaudeCliInvoiceExtractor.Parse(envelope);
|
||
inv.SupplierName.Should().Be("X");
|
||
inv.Lines.Should().ContainSingle();
|
||
inv.InputTokens.Should().Be(123);
|
||
inv.OutputTokens.Should().Be(45);
|
||
}
|
||
|
||
[Fact]
|
||
public void Parse_barcode_with_spaces_is_stripped()
|
||
{
|
||
var json = """{"lines":[{"name":"X","qty":1,"price":1,"barcode":"4870 123 456 789"}]}""";
|
||
var inv = ClaudeCliInvoiceExtractor.Parse(json);
|
||
inv.Lines[0].Barcode.Should().Be("4870123456789");
|
||
}
|
||
|
||
[Fact]
|
||
public void Parse_null_fields_tolerated()
|
||
{
|
||
var json = """{"supplier":null,"number":null,"date":null,"total":null,"lines":[{"name":"X","qty":"2,5","price":"100.5"}]}""";
|
||
var inv = ClaudeCliInvoiceExtractor.Parse(json);
|
||
inv.SupplierName.Should().BeNull();
|
||
inv.TotalSum.Should().BeNull();
|
||
inv.Lines[0].Quantity.Should().Be(2.5m); // «2,5» → 2.5
|
||
inv.Lines[0].Price.Should().Be(100.5m);
|
||
}
|
||
|
||
[Fact]
|
||
public void Parse_empty_raises()
|
||
{
|
||
var act = () => ClaudeCliInvoiceExtractor.Parse("");
|
||
act.Should().Throw<InvoiceExtractionException>();
|
||
}
|
||
|
||
[Fact]
|
||
public void Parse_lines_without_name_are_skipped()
|
||
{
|
||
var json = """{"lines":[{"name":"A","qty":1,"price":1},{"name":"","qty":2,"price":2},{"name":null,"qty":3,"price":3}]}""";
|
||
var inv = ClaudeCliInvoiceExtractor.Parse(json);
|
||
inv.Lines.Should().ContainSingle(l => l.Name == "A");
|
||
}
|
||
}
|