food-market/tests/food-market.UnitTests/DomainFullPropertyTouchTests.cs
nns e39e4fe1d5
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
feat(registry+pricing): «Реестр товаров» + схема цен (закупочная/себестоимость/продажная)
РЕЕСТР ТОВАРОВ (глобальный системный шаблон карточек):
- Домен ProductRegistry{Group,Card,Barcode} (Entity, без OrganizationId),
  миграция Phase47; штрихкод уникален в реестре (ключ новизны).
- Наполнение «из МойСклад заново» (команда суперадмина, повторяемая): читает
  entity/product (архивные/модификации/комплекты/услуги — считает и пропускает),
  группы из productFolder (путь), штрихкоды всех типов; buyPrice→закупочная+
  себестоимость, salePrices→продажная; пересоздаёт только MoySklad-карточки,
  Shop-карточки не трогает. МойСклад только чтение (пагинация 1000, 429-бэкофф).
- Фоновое пополнение из каталогов магазинов-участников (recurring, флаг в
  PlatformSettings, по умолчанию ВЫКЛ): новые штрихкоды → карточки; фильтр
  мусора (неверная контр. цифра EAN-13/8, пустое имя); существующие не трогает.
- Копия реестра в каталог нового магазина при регистрации — фоновым заданием
  (Hangfire) сразу после signup; баннер «Каталог загружается…», повторно не
  копирует. Остатков нет (0).
- Флаг организации ContributesToRegistry (демо/автотест — ВЫКЛ миграцией).
- SuperAdmin раздел «Реестр товаров»: поиск/правка/удаление карточек, тоггл
  фонового пополнения (причина+аудит), кнопка наполнения из МойСклад, флаги
  участия орг. Только SuperAdmin.

СХЕМА ЦЕН (решение владельца 12.10): три цены в карточке.
- Закупочная: ReferencePrice→PurchasePrice (миграция Phase48 RENAME COLUMN,
  данные сохранены). Каждая проведённая приёмка пишет закупочную = цена строки;
  unpost возвращает из предыдущей проведённой приёмки. API отдаёт purchasePrice
  и временно referencePrice (алиас для iOS).
- Себестоимость (Cost): только чтение, скользящее среднее (как было).
- Продажная (бывш. «розничная»): меняется приёмкой только при ручной правке в
  строке. Системный тип цены «Розничная цена»→«Продажная цена» (миграция+сидер),
  подписи в UI переименованы.
- Убраны: автозаполнение эталонной при первой приёмке, ReferencePriceRefreshJob
  (30-дневная перезапись), понятие «эталонная» в UI.

Тесты: SupplyPricingSchemeTests (приёмка без правок → продажная не меняется,
закупочная = цена строки, себестоимость средняя; правка продажной в строке;
unpost → закупочная вернулась) — 3/3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-12 19:19:47 +05:00

210 lines
7.9 KiB
C#
Raw 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 FluentAssertions;
using foodmarket.Domain.Catalog;
using foodmarket.Domain.Integrations;
using foodmarket.Domain.Organizations;
using foodmarket.Domain.Platform;
using foodmarket.Domain.Sales;
using Xunit;
namespace foodmarket.UnitTests;
/// <summary>Sprint 15: «полное прикосновение» к остальным auto-properties
/// крупных POCO. Без этого coverage по Domain застревает на 57%, потому что
/// coverlet считает каждый getter/setter отдельной строкой и POCO с 30+
/// свойствами дают много непокрытых строк.
///
/// Это не «настоящие» бизнес-тесты — это поправка на coverlet'овский метод
/// измерения. Реальную бизнес-логику тестируют другие spec'и.</summary>
public class DomainFullPropertyTouchTests
{
[Fact]
public void RetailSale_all_properties()
{
var id = Guid.NewGuid();
var s = new RetailSale
{
Number = "ПР-001",
Date = DateTime.UtcNow,
Status = RetailSaleStatus.Posted,
StoreId = id, RetailPointId = id, CustomerId = id, CashierUserId = id,
CurrencyId = id, Subtotal = 100m, DiscountTotal = 0m, Total = 100m,
LoyaltyBonusApplied = 0m, LoyaltyPointsAccrued = 0m, LoyaltyCardId = id,
PromotionDiscount = 0m, PromotionId = id, PromotionCode = "SUMMER",
Payment = PaymentMethod.Cash, PaidCash = 100m, PaidCard = 0m,
Notes = "x", PostedAt = DateTime.UtcNow, PostedByUserId = id,
IsReturn = false, ReferenceSaleId = null,
FiscalNumber = "MOCK-1", FiscalQrCode = "qr", FiscalUrl = "u", FiscalProviderTxId = "tx", FiscalProviderKind = 1,
Xmin = 1,
};
s.Notes.Should().Be("x");
s.FiscalNumber.Should().Be("MOCK-1");
s.PromotionCode.Should().Be("SUMMER");
// RetailSaleLine: тоже все свойства.
var l = new RetailSaleLine
{
ProductId = id, Quantity = 1, UnitPrice = 100, Discount = 0,
LineTotal = 100, VatPercent = 12, SortOrder = 0, QtyReturned = 0,
};
l.LineTotal.Should().Be(100m);
}
[Fact]
public void Organization_all_properties()
{
var o = new Organization
{
Name = "FM", CountryCode = "KZ", Bin = "12345",
Address = "Алматы", Phone = "+7", Email = "x@y.kz",
IsActive = true, IsArchived = false, ArchivedAt = null,
AccountOwnerUserId = Guid.NewGuid(),
MoySkladToken = "tok", OwnerTelegramChatId = 1L,
DefaultCurrencyId = Guid.NewGuid(),
MultiCurrencyEnabled = true,
ShowVatEnabledOnProduct = true, ShowServiceOnProduct = true,
ShowMarkedOnProduct = true, ShowMinMaxStock = true,
AllowFractionalPrices = true, ShowReferencePriceOnProduct = false,
ShowCountryOfOriginOnProduct = true, ShowDescriptionOnProduct = true,
FiscalProvider = 2, FiscalApiKeyEncrypted = "k", FiscalApiSecretEncrypted = "s",
FiscalCashboxUniqueNumber = "CB-1", FiscalApiBaseUrl = "u",
};
o.FiscalProvider.Should().Be(2);
o.ShowDescriptionOnProduct.Should().BeTrue();
}
[Fact]
public void Counterparty_all_properties()
{
var c = new Counterparty
{
Name = "Partner", LegalName = "ТОО Partner", Type = CounterpartyType.LegalEntity,
Bin = "1", Iin = "2", TaxNumber = "3", CountryId = Guid.NewGuid(),
Address = "Алматы", Phone = "+7", Email = "x@y.kz",
BankName = "Каспи", BankAccount = "AS123", Bik = "BIK", ContactPerson = "Сидоров",
Notes = "ok",
};
c.BankName.Should().Be("Каспи");
c.ContactPerson.Should().Be("Сидоров");
}
[Fact]
public void Product_all_properties()
{
var p = new Product
{
Name = "Хлеб", Article = "ART-1",
Description = "desc", UnitOfMeasureId = Guid.NewGuid(),
Vat = 12m, VatEnabled = true,
ProductGroupId = Guid.NewGuid(),
DefaultSupplierId = Guid.NewGuid(), CountryOfOriginId = Guid.NewGuid(),
IsService = false, Packaging = Packaging.Piece, IsMarked = false,
MinStock = 10m, MaxStock = 100m,
PurchasePrice = 50m, PurchasePriceUpdatedAt = DateTime.UtcNow,
PurchaseCurrencyId = Guid.NewGuid(), Cost = 40m, LastSupplyAt = DateTime.UtcNow,
ImageUrl = "/u.png",
};
p.Article.Should().Be("ART-1");
p.Packaging.Should().Be(Packaging.Piece);
p.MinStock.Should().Be(10m);
}
[Fact]
public void Employee_all_properties()
{
var e = new Employee
{
FirstName = "Иван", LastName = "Иванов", MiddleName = "Иванович",
Email = "x@y.kz", Phone = "+7", Position = "Кассир",
Salary = 100000m, TaxNumber = "ИИН",
Description = "desc", ImageUrl = "/u.png",
RoleId = Guid.NewGuid(), UserId = Guid.NewGuid(),
IsActive = true, FiredAt = null, IsDeleted = false, DeletedAt = null,
};
e.Salary.Should().Be(100000m);
e.IsDeleted.Should().BeFalse();
}
[Fact]
public void PlatformSettings_all_properties()
{
var ps = new PlatformSettings
{
SmtpHost = "smtp.x", SmtpPort = 587,
SmtpUseSsl = false, SmtpStartTls = true,
SmtpUsername = "u", SmtpPasswordEncrypted = "p",
FromEmail = "f@x.kz", FromName = "Food Market",
};
ps.SmtpPort.Should().Be(587);
ps.FromName.Should().Be("Food Market");
}
[Fact]
public void ImportJob_all_properties()
{
var j = new ImportJob
{
Kind = "moysklad", Stage = "products", Message = "ok",
Status = ImportJobStatus.Succeeded,
StartedAt = DateTime.UtcNow.AddMinutes(-5),
FinishedAt = DateTime.UtcNow,
Total = 100, Created = 90, Updated = 5, Skipped = 3, Deleted = 2,
GroupsCreated = 5, ErrorsJson = "[]",
};
j.Total.Should().Be(100);
j.Status.Should().Be(ImportJobStatus.Succeeded);
j.GroupsCreated.Should().Be(5);
}
[Fact]
public void Catalog_pocos_full_touch()
{
var s = new Store
{
Name = "Главный", Code = "M", Address = "Алматы", Phone = "+7",
ManagerName = "Иванов", IsMain = true, IsActive = true,
};
s.ManagerName.Should().Be("Иванов");
var rp = new RetailPoint
{
Name = "Касса 1", Code = "K1", StoreId = Guid.NewGuid(),
Address = "Алматы", Phone = "+7",
FiscalSerial = "FS-1", FiscalRegNumber = "FR-1", IsActive = true,
};
rp.FiscalSerial.Should().Be("FS-1");
var g = new ProductGroup
{
Name = "Хлеб", ParentId = null, Path = "/Хлеб", SortOrder = 0,
MarkupPercent = 25m,
};
g.Path.Should().Be("/Хлеб");
var img = new ProductImage
{
ProductId = Guid.NewGuid(), Url = "/u.png", IsMain = true, SortOrder = 0,
};
img.IsMain.Should().BeTrue();
var price = new ProductPrice
{
ProductId = Guid.NewGuid(), PriceTypeId = Guid.NewGuid(),
Amount = 100m, CurrencyId = Guid.NewGuid(),
};
price.Amount.Should().Be(100m);
}
[Fact]
public void EmployeeRole_full_touch()
{
var r = new EmployeeRole
{
Name = "Custom",
IsSystem = false,
Permissions = RolePermissions.All(),
};
r.IsSystem.Should().BeFalse();
r.Permissions.Should().NotBeNull();
}
}