using FluentAssertions; using foodmarket.Api.Infrastructure.Invoices; using Xunit; namespace foodmarket.UnitTests; /// Проверка парсинга ответа Claude CLI. CLI отдаёт конверт /// { result: "…json…", usage: {...} }; result иногда обёрнут в /// ```json. Нужно вытащить payload и привести к ParsedInvoice. 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(); } [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"); } }