Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Диапазон коммитов f455fa9..b7eb637 + 3ced706 (handoff).
Backend (src/food-market.api/Infrastructure/Invoices):
- Ean.cs — контрольная цифра EAN-8/13/UPC-A/GTIN-14 (ловит одну ошибку в цифре).
- TableReader.cs — своя читалка .xlsx/.csv без библиотек, UTF-8/1251,
.xls → понятная ошибка; TableInvoiceParser — шаблон накладной.
- InvoiceModels: +Photo/BarcodeSource/Note в InvoiceLine, PhotoHint,
IInvoiceExtractor.ExtractPhotosAsync.
- InvoiceMatcher.FindByBarcode — для photo-matching.
- InvoiceProcessor: ветки photos/excel/vision, MatchFromPhoto с EAN-проверкой
(barcode_source="read" → валидация), SweepAbandonedPhotosAsync раз в час
убирает папки needs_review старше 24ч; DeleteStored (owner-policy: НЕ копить).
- ClaudeCliInvoiceExtractor: ExtractPhotosAsync + BuildPhotosPrompt +
FromPhotosJson (barcode_source → BarcodeSource, comment → Note, unmatched_photos
→ Warnings); дефолт модели claude-sonnet-5-5, PhotoTimeoutSeconds=600.
Backend (Controllers):
- POST /purchases/invoices/upload-photos (≤80 файлов, 60 МБ) +
GET /purchases/invoices/{id}/photo/{n}; Apply/Delete убирают папку (не только файл).
- InvoiceDetailDto +Source/+HasFiles — клиент не ломится за 404 миниатюрами.
- POST /inventory/inventories/import — xlsx/csv → {found, unknown}, повторы ШК
суммируются, документ не создаётся.
- Program.cs: UseResponseCompression + GzipCompressionProvider для application/json.
- InventoriesController.mobile-catalog: weak ETag + If-None-Match → 304;
Cache-Control private max-age=60. Замер 31k: JSON 3.60 МБ → gzip 0.98 МБ.
Frontend:
- lib/shots.ts — ZXing TRY_HARDER на 1x+1600; compress JPEG 0.85 до 2400.
- components/PhotoSession.tsx — полноэкранная пачка (камера + готовые фото).
- components/CameraScanner.tsx: onCodeRef + эффект [], камера не закрывается
после каждого скана.
- pages/InvoicesPage.tsx — четыре способа: камера одиночными, файл (xlsx/csv/img/pdf),
камера многостраничная, фото-приёмка (камера или готовые фото) + «Отправить ещё раз».
- pages/InvoiceReviewPage.tsx — автосохранение 700 мс + индикатор; row-picker панель
«в какую строку?» если fuzzy-match неуверен; linesRef; глобальный keydown; миниатюра
снимка у строки; пометки Note.
- pages/SupplyScanPage.tsx + InventoryMobilePage.tsx — глобальный keydown.
- InventoryMobilePage: поле сканера на виду + импорт Excel/CSV.
- docs/supplier-invoice-template.xlsx — шаблон для поставщиков (9-я строка заголовки).
Хранение файлов (owner-policy 2026-10-05): фото НЕ копим.
- Обычная загрузка → удаление сразу после разбора.
- Фото-приёмка → удаление при Apply/Delete/24h abandoned.
- KeepFilesDays удалён из флоу (конфиг-ключ игнорируется).
Тесты (unit, зелёные):
- EanTests (6): валидные/невалидные/one-digit error.
- TableReaderTests (5): xlsx cells, шапка «Поставщик/Номер/Дата», CSV UTF-8+BOM,
CSV Windows-1251, .xls friendly error.
- ClaudeCliInvoicePhotosParseTests (4): decoded проходит, source=null чистит Barcode,
unmatched_photos → Warnings, comment → Note.
- InvoicePhotoRetentionTests (3): DeleteStored файл/папка/идемпотентность.
Preexisting ApiReferenceDocsJob (Sprint 28) failure не связан с портом.
Прогресс: docs/sklad-port-20261005-progress.md. Пользовательская инструкция:
docs/supply-inventory.md обновлена.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
75 lines
2.8 KiB
C#
75 lines
2.8 KiB
C#
using FluentAssertions;
|
||
using foodmarket.Api.Infrastructure.Invoices;
|
||
using Xunit;
|
||
|
||
namespace foodmarket.UnitTests;
|
||
|
||
/// <summary>Разбор JSON-ответа модели для фото-приёмки (handoff §5): к обычной
|
||
/// шапке добавляются <c>photo</c>, <c>barcode_source</c> (invoice|decoded|read),
|
||
/// <c>comment</c> → <c>Note</c>, и <c>unmatched_photos</c> → <c>Warnings</c>.
|
||
/// barcode_source=null (или отсутствует) означает «цифры не прочитаны уверенно»
|
||
/// и мы обнуляем <c>Barcode</c>, чтобы не подставить что-то наугад.</summary>
|
||
public class ClaudeCliInvoicePhotosParseTests
|
||
{
|
||
[Fact]
|
||
public void Photos_decoded_source_passes_through()
|
||
{
|
||
var json = """
|
||
{
|
||
"supplier":"ТОО X","number":"7","date":"2026-10-05","total":4500,
|
||
"invoice_photos":[1],
|
||
"lines":[
|
||
{"name":"Молоко 3.2%","qty":10,"price":450,"sum":4500,
|
||
"barcode":"4870001234561","barcode_source":"decoded","photo":2,"comment":null}
|
||
],
|
||
"unmatched_photos":[]
|
||
}
|
||
""";
|
||
var inv = ClaudeCliInvoiceExtractor.FromPhotosJson(json);
|
||
inv.Lines.Should().HaveCount(1);
|
||
var l = inv.Lines[0];
|
||
l.Barcode.Should().Be("4870001234561");
|
||
l.BarcodeSource.Should().Be("decoded");
|
||
l.Photo.Should().Be(2);
|
||
inv.Warnings.Should().BeEmpty();
|
||
}
|
||
|
||
[Fact]
|
||
public void Photos_source_null_clears_barcode()
|
||
{
|
||
// Если модель не уверена в цифрах, barcode_source=null → штрихкод не берём,
|
||
// чтобы не привязать строку к чужому товару.
|
||
var json = """
|
||
{"supplier":"X","lines":[
|
||
{"name":"A","qty":1,"price":10,"barcode":"4870000000000","barcode_source":null,"photo":null}
|
||
]}
|
||
""";
|
||
var inv = ClaudeCliInvoiceExtractor.FromPhotosJson(json);
|
||
inv.Lines[0].Barcode.Should().BeNull();
|
||
inv.Lines[0].BarcodeSource.Should().BeNull();
|
||
}
|
||
|
||
[Fact]
|
||
public void Unmatched_photos_become_warning()
|
||
{
|
||
var json = """
|
||
{"supplier":"X","lines":[{"name":"A","qty":1,"price":10}],
|
||
"unmatched_photos":[5,7]}
|
||
""";
|
||
var inv = ClaudeCliInvoiceExtractor.FromPhotosJson(json);
|
||
inv.Warnings.Should().ContainMatch("*5*7*");
|
||
}
|
||
|
||
[Fact]
|
||
public void Comment_lands_in_note()
|
||
{
|
||
var json = """
|
||
{"supplier":"X","lines":[
|
||
{"name":"А","qty":1,"price":10,"barcode_source":"invoice","comment":"сверьте вес"}
|
||
]}
|
||
""";
|
||
var inv = ClaudeCliInvoiceExtractor.FromPhotosJson(json);
|
||
inv.Lines[0].Note.Should().Be("сверьте вес");
|
||
}
|
||
}
|