food-market/tests/food-market.IntegrationTests/CabinetSetupTests.cs
nns 519e4073df
Some checks failed
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 Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions
Docker API / Build + push API (push) Has been cancelled
Docker API / Deploy API on stage (push) Has been cancelled
fix(кабинет): этап «Настройка кабинета» — 15 правок (прогон 14.10)
1. Реквизиты организации: БИН/ИИН (12 цифр, проверка), юр.адрес, «Плательщик
   НДС» — в Настройках → Организация, сохраняются и отдаются в API (Organization.
   IsVatPayer + Bin/Address в OrgSettings).
2. Склад при регистрации — адрес пустой (была заглушка «Алматы, ул. Пример 1»);
   миграция чистит заглушку у существующих складов.
3. Кассы: блок скачивания кассы скрыт, пока нет ссылок на сборки (убраны
   мёртвые кнопки и технический текст про конфигурацию сервера).
4. Счета: в списке показывается валюта счёта (а не ₸ орг.); новый счёт по
   умолчанию в валюте организации (KZT), не первой в справочнике. Миграция:
   счета KZ-организаций в BYN без движений → KZT.
5. «Дата начала учёта» берётся по локальной дате (была UTC → ночью подставляла
   вчера). todayIso() переведён на локальное время.
6. Esc в выпадающем списке закрывает только список, не весь диалог (Select гасит
   всплытие + Modal закрывается лишь при !defaultPrevented).
7. Счёт-касса: подпись «Магазин (к которому относится касса)» вместо «привязана
   к точке» — список показывает склады/магазины.
8. Точка продаж: поля «Счёт для наличных» и «Счёт для оплаты картой»; автооткрытие
   смены берёт счета из точки (наличные→Cash, карта→Acquiring/Bank).
9. PIN кассира: в карточке сотрудника задать/сменить/снять PIN; в списке «PIN
   задан»; вход grant_type=pin по точке (сервер уже поддерживал).
10. «Сбросить пароль» в карточке сотрудника → новый временный пароль один раз.
11. Владелец в «Сотрудниках» — «Владелец» вместо «<название> Owner» (signup +
    миграция существующих).
12. Роли: матрица прав дополнена недостающими 6 правами (перемещения, деньги/
    смены, маркетинг) — цифра «N/38» теперь совпадает с галочками; текст без
    жаргона «CRUD».
13. Настройки: блок «Демо-данные» — только суперадмину; «Креды per-tenant» →
    человеческий текст.
15. Форма входа: сообщения «Укажите email» / «Введите пароль» (как в регистрации).
16. Суммы счетов без копеек, если в организации выключены дробные цены
    (useFormatCurrency учитывает allowFractionalPrices).

Тесты: CabinetSetupTests (реквизиты+валидация БИН, имя владельца, счета точки,
PIN+вход grant_type=pin, сброс пароля) 5/5; регрессия Plan/Markup/RegistryCopy 7/7.
Код весовых (27+5) НЕ трогал — весы на паузе. П.14 — см. отчёт (блок продажи в
минус не ставится; поведение «разрешено с предупреждением» уже соответствует).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-14 01:55:29 +05:00

179 lines
8.6 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>Этап «Настройка кабинета» (14.10): реквизиты организации (п.1),
/// счета точки продаж (п.8), PIN кассира + вход grant_type=pin (п.9), сброс
/// пароля сотрудника (п.10), имя владельца «Владелец» (п.11).</summary>
[Collection(ApiCollection.Name)]
public class CabinetSetupTests
{
private readonly ApiFactory _factory;
public CabinetSetupTests(ApiFactory factory) => _factory = factory;
private static async Task<Dictionary<string, JsonElement>> SettingsDictAsync(ApiActor api)
{
var s = await api.GetJsonAsync("/api/organization/settings");
return JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(s.GetRawText())!;
}
[Fact]
public async Task Requisites_save_and_validate_bin()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"req-{Guid.NewGuid():N}");
var dict = await SettingsDictAsync(api);
dict["bin"] = JsonSerializer.SerializeToElement("123456789012");
dict["address"] = JsonSerializer.SerializeToElement("Алматы, Абая 1");
dict["isVatPayer"] = JsonSerializer.SerializeToElement(false);
(await api.Http.PutAsJsonAsync("/api/organization/settings", dict)).EnsureSuccessStatusCode();
var after = await api.GetJsonAsync("/api/organization/settings");
after.GetProperty("bin").GetString().Should().Be("123456789012");
after.GetProperty("address").GetString().Should().Be("Алматы, Абая 1");
after.GetProperty("isVatPayer").GetBoolean().Should().BeFalse();
// БИН неверной длины — 400.
var bad = await SettingsDictAsync(api);
bad["bin"] = JsonSerializer.SerializeToElement("123");
var resp = await api.Http.PutAsJsonAsync("/api/organization/settings", bad);
resp.StatusCode.Should().Be(HttpStatusCode.BadRequest);
}
[Fact]
public async Task Owner_employee_named_vladelec()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"own-{Guid.NewGuid():N}");
var list = await api.ListAsync("/api/organization/employees?pageSize=50");
var owner = list.FirstOrDefault(e => e.GetProperty("isOwner").GetBoolean());
owner.ValueKind.Should().Be(JsonValueKind.Object, "владелец есть в списке сотрудников");
owner.GetProperty("lastName").GetString().Should().Be("Владелец");
owner.GetProperty("firstName").GetString().Should().Be("");
}
[Fact]
public async Task RetailPoint_stores_cash_and_card_accounts()
{
var api = new ApiActor(_factory.CreateClient());
await api.SignupAndLoginAsync($"rp-{Guid.NewGuid():N}");
var refs = await api.LoadRefsAsync();
// Создаём Cash и Acquiring счета.
var cash = await CreateAccountAsync(api, "Касса", kind: 1, refs.CurrencyId, storeId: refs.StoreId);
var card = await CreateAccountAsync(api, "Эквайринг", kind: 4, refs.CurrencyId, storeId: null);
var point = (await api.ListAsync("/api/catalog/retail-points?pageSize=50")).First();
var pointId = point.GetProperty("id").GetString()!;
var put = await api.Http.PutAsJsonAsync($"/api/catalog/retail-points/{pointId}", new
{
name = point.GetProperty("name").GetString(),
code = point.GetProperty("code").GetString(),
storeId = point.GetProperty("storeId").GetString(),
address = (string?)null, phone = (string?)null,
fiscalSerial = (string?)null, fiscalRegNumber = (string?)null, isActive = true,
cashAccountId = cash, cardAccountId = card,
});
put.EnsureSuccessStatusCode();
var after = await api.GetJsonAsync($"/api/catalog/retail-points/{pointId}");
after.GetProperty("cashAccountId").GetString().Should().Be(cash);
after.GetProperty("cardAccountId").GetString().Should().Be(card);
}
[Fact]
public async Task Cashier_pin_set_and_pin_login()
{
var api = new ApiActor(_factory.CreateClient());
var slug = $"pin-{Guid.NewGuid():N}";
await api.SignupAndLoginAsync(slug);
var cashierRoleId = (await api.ListAsync("/api/organization/employee-roles?pageSize=50"))
.First(r => r.GetProperty("name").GetString() == "Кассир").GetProperty("id").GetString();
var pointId = (await api.ListAsync("/api/catalog/retail-points?pageSize=50")).First().GetProperty("id").GetString()!;
var cashierEmail = $"cashier-{slug}@example.kz";
var createResp = await api.Http.PostAsJsonAsync("/api/organization/employees", new
{
lastName = "Кассирова", firstName = "Айгерим", roleId = cashierRoleId, isActive = true,
email = cashierEmail, createAccount = true,
retailPointIds = new[] { pointId },
});
createResp.EnsureSuccessStatusCode();
var empId = (await createResp.Content.ReadFromJsonAsync<JsonElement>())
.GetProperty("employee").GetProperty("id").GetString();
(await api.Http.PostAsJsonAsync($"/api/organization/employees/{empId}/pin", new { pin = "1234" }))
.EnsureSuccessStatusCode();
// Факт «PIN задан» виден в списке.
var emp = await api.GetJsonAsync($"/api/organization/employees/{empId}");
emp.GetProperty("pinSet").GetBoolean().Should().BeTrue();
// Вход grant_type=pin по точке.
using var anon = _factory.CreateClient();
var tok = await anon.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "pin",
["retail_point_id"] = pointId,
["pin"] = "1234",
["client_id"] = "food-market-web",
["scope"] = "openid profile email roles api offline_access",
}));
var body = await tok.Content.ReadAsStringAsync();
tok.IsSuccessStatusCode.Should().BeTrue(body);
(await tok.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("access_token").GetString()
.Should().NotBeNullOrEmpty();
}
[Fact]
public async Task Reset_password_issues_working_password()
{
var api = new ApiActor(_factory.CreateClient());
var slug = $"rst-{Guid.NewGuid():N}";
await api.SignupAndLoginAsync(slug);
var roleId = (await api.ListAsync("/api/organization/employee-roles?pageSize=50"))
.First(r => r.GetProperty("name").GetString() == "Кладовщик").GetProperty("id").GetString();
var email = $"mgr-{slug}@example.kz";
var createResp = await api.Http.PostAsJsonAsync("/api/organization/employees", new
{
lastName = "Петров", firstName = "Пётр", roleId, isActive = true,
email, createAccount = true,
});
createResp.EnsureSuccessStatusCode();
var empId = (await createResp.Content.ReadFromJsonAsync<JsonElement>())
.GetProperty("employee").GetProperty("id").GetString();
var reset = await api.Http.PostAsJsonAsync($"/api/organization/employees/{empId}/reset-password", new { });
reset.IsSuccessStatusCode.Should().BeTrue(await reset.Content.ReadAsStringAsync());
var newPassword = (await reset.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("password").GetString()!;
newPassword.Should().NotBeNullOrEmpty();
// Новый пароль работает.
using var anon = _factory.CreateClient();
var login = new ApiActor(anon);
var token = await login.TokenAsync(email, newPassword);
token.Should().NotBeNullOrEmpty();
}
private static async Task<string> CreateAccountAsync(ApiActor api, string name, int kind, string currencyId, string? storeId)
{
var resp = await api.Http.PostAsJsonAsync("/api/money/accounts", new
{
name, kind, bankName = (string?)null, accountNumber = (string?)null,
storeId, currencyId, openingBalance = 0m, openingDate = DateTime.UtcNow,
isActive = true, isDefault = false,
});
resp.IsSuccessStatusCode.Should().BeTrue(await resp.Content.ReadAsStringAsync());
return (await resp.Content.ReadFromJsonAsync<JsonElement>()).GetProperty("id").GetString()!;
}
}