food-market/tests/food-market.IntegrationTests/ProductCardParityTests.cs
nns 0ca0d6cc3b
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
feat(catalog): карточка товара как МойСклад+ — обязательно только наименование
Правки владельца 08.10 (п.4 паритета):
- Сохранить можно при наличии Наименования. Единица не обязательна в запросе →
  дефолт «шт»; группа → корень. Штрихкод ОБЯЗАТЕЛЕН у любого товара: нет в запросе/
  импорте → автогенерация внутреннего EAN-13 (верная контр. цифра, префикс не
  пересекается с весовыми/ценовыми префиксами орг. — InternalBarcode).
- Цена продажи не обязательна для сохранения; но ПРОДАТЬ без неё нельзя — касса и
  розничная продажа дают «У товара «X» нет цены продажи» (можно ввести вручную).
- Быстрое создание из кассы/приёмки: POST /api/catalog/products/quick (имя + опц.
  штрихкод/цена/весовой).
- Снят FluentValidation NotEqual(Guid.Empty) на единицу (контроллер подставляет дефолт).

Тесты: unit InternalBarcode 3/3 (валидный EAN-13, не парсится как весовой, обходит
префиксы); integration 3/3 (создание по одному имени, продажа без цены → 400, quick).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 05:45:38 +05:00

85 lines
4.3 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 System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using FluentAssertions;
using foodmarket.IntegrationTests.Support;
using Xunit;
namespace foodmarket.IntegrationTests;
/// <summary>Паритет карточки товара с МойСкладом + правки владельца 08.10:
/// обязательно только наименование; единица/группа по умолчанию; штрихкод
/// обязателен (автогенерация, если не задан); цена продажи не обязательна для
/// сохранения, но продать без неё нельзя.</summary>
[Collection(ApiCollection.Name)]
public class ProductCardParityTests
{
private readonly ApiFactory _factory;
public ProductCardParityTests(ApiFactory factory) => _factory = factory;
private static string RandomBarcode()
=> string.Concat(Enumerable.Range(0, 13).Select(_ => Random.Shared.Next(0, 10)));
[Fact]
public async Task Create_with_only_name_succeeds_autogenerates_barcode_and_defaults()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"card-{Guid.NewGuid():N}");
// Только наименование — ни единицы, ни группы, ни штрихкода, ни цены.
var resp = await api.Http.PostAsJsonAsync("/api/catalog/products", new { name = "Товар без ничего" });
var body = await resp.Content.ReadAsStringAsync();
resp.StatusCode.Should().Be(HttpStatusCode.Created, body);
var p = JsonDocument.Parse(body).RootElement;
var id = p.GetProperty("id").GetString();
// Штрихкод автогенерирован (13 цифр), единица проставлена.
var full = await api.GetJsonAsync($"/api/catalog/products/{id}");
var barcodes = full.GetProperty("barcodes").EnumerateArray().ToList();
barcodes.Should().HaveCount(1, "штрихкод обязателен → автогенерация");
barcodes[0].GetProperty("code").GetString().Should().MatchRegex("^[0-9]{13}$");
full.GetProperty("unitName").GetString().Should().NotBeNullOrEmpty();
}
[Fact]
public async Task Sale_of_product_without_price_is_rejected_with_clear_message()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"noprice-{Guid.NewGuid():N}");
var refs = await api.LoadRefsAsync();
// Товар без цены продажи.
var create = await api.Http.PostAsJsonAsync("/api/catalog/products", new { name = "Без цены" });
create.StatusCode.Should().Be(HttpStatusCode.Created);
var pid = JsonDocument.Parse(await create.Content.ReadAsStringAsync()).RootElement.GetProperty("id").GetString();
var sale = await api.Http.PostAsJsonAsync("/api/sales/retail", new
{
date = DateTime.UtcNow, storeId = refs.StoreId, currencyId = refs.CurrencyId,
payment = 0, paidCash = 0m, paidCard = 0m, isReturn = false,
lines = new[] { new { productId = pid, quantity = 1m, unitPrice = 0m, discount = 0m, vatPercent = 12m, sortOrder = 0 } },
});
sale.StatusCode.Should().Be(HttpStatusCode.BadRequest);
(await sale.Content.ReadAsStringAsync()).Should().Contain("нет цены продажи");
}
[Fact]
public async Task QuickCreate_by_barcode_creates_minimal_product()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"quick-{Guid.NewGuid():N}");
var bc = RandomBarcode();
var resp = await api.Http.PostAsJsonAsync("/api/catalog/products/quick",
new { name = "Быстрый товар", barcode = bc, salePrice = 250m });
var body = await resp.Content.ReadAsStringAsync();
resp.StatusCode.Should().Be(HttpStatusCode.Created, body);
var id = JsonDocument.Parse(body).RootElement.GetProperty("id").GetString();
// Находится по тому самому штрихкоду.
var byBc = await api.GetJsonAsync($"/api/catalog/products/by-barcode/{bc}");
// by-barcode возвращает товар или {items:[…]}; проверим, что наш id присутствует.
byBc.GetRawText().Should().Contain(id!);
}
}