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
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>
179 lines
8.6 KiB
C#
179 lines
8.6 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>Этап «Настройка кабинета» (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()!;
|
||
}
|
||
}
|