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
РЕЕСТР ТОВАРОВ (глобальный системный шаблон карточек):
- Домен 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>
210 lines
7.9 KiB
C#
210 lines
7.9 KiB
C#
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();
|
||
}
|
||
}
|