food-market/tests/food-market.UnitTests/ClaudeCliInvoiceExtractorParseTests.cs
nns 457d8f5877 test(s31): unit 20/20 + integration 2/2 для invoice pipeline (7/8)
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>
2026-10-04 21:46:10 +05:00

80 lines
3.2 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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");
}
}