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
Правки владельца 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>
85 lines
4.3 KiB
C#
85 lines
4.3 KiB
C#
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!);
|
||
}
|
||
}
|