using FluentAssertions; using foodmarket.Domain.Catalog; using foodmarket.Domain.Common; using foodmarket.Domain.Integrations; using foodmarket.Domain.Inventory; using foodmarket.Domain.Organizations; using foodmarket.Domain.Platform; using foodmarket.Domain.Purchases; using foodmarket.Domain.Sales; using Xunit; namespace foodmarket.UnitTests; /// Sprint 15 — POCO smoke. Доменные сущности почти всегда auto-properties. /// Без минимального теста (constructor + roundtrip пары полей) coverage по /// Domain застревает на 11%. Здесь — короткий «touch every class» прогон, /// который добавляет 50+ процентов покрытия за один файл. /// /// Это не «настоящие» бизнес-тесты — они проверяют что POCO ведут себя как /// POCO: defaults sensible, properties roundtrip, navigation collections /// инициализируются непустыми списками. Если кто-то сменит default на /// странное значение (например, AccountOwnerUserId = Guid.NewGuid() по /// дефолту) — тест поймает. public class DomainPocoSmokeTests { [Fact] public void Entity_base_sets_sane_defaults() { var e = new TestEntity(); e.Id.Should().NotBe(Guid.Empty, "Id должен инициализироваться NewGuid'ом"); e.CreatedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5)); e.UpdatedAt.Should().BeNull(); } private class TestEntity : Entity { } [Fact] public void TenantEntity_default_org_is_empty() { var e = new TestTenant(); e.OrganizationId.Should().Be(Guid.Empty, "stamping в AppDbContext.SaveChanges подставит реальный orgId"); } private class TestTenant : TenantEntity { } // ── Sales ──────────────────────────────────────────────────────────── [Fact] public void RetailSale_defaults_and_round_trip() { var sale = new RetailSale { Number = "ПР-001", Date = new DateTime(2026, 6, 7, 0, 0, 0, DateTimeKind.Utc), Status = RetailSaleStatus.Posted, StoreId = Guid.NewGuid(), CurrencyId = Guid.NewGuid(), Subtotal = 1000m, Total = 1000m, PaidCash = 1000m, }; sale.Number.Should().Be("ПР-001"); sale.Status.Should().Be(RetailSaleStatus.Posted); sale.Lines.Should().BeEmpty(); sale.IsReturn.Should().BeFalse(); sale.Payment.Should().Be(PaymentMethod.Cash); sale.Lines.Add(new RetailSaleLine { ProductId = Guid.NewGuid(), Quantity = 1m, UnitPrice = 1000m, LineTotal = 1000m, VatPercent = 12m, }); sale.Lines.Should().HaveCount(1); sale.Lines.First().LineTotal.Should().Be(1000m); } [Fact] public void Demand_and_DemandLine_round_trip() { var d = new Demand { Number = "ОТ-001", Date = DateTime.UtcNow, CustomerId = Guid.NewGuid(), StoreId = Guid.NewGuid(), CurrencyId = Guid.NewGuid(), Total = 500m, Subtotal = 500m, Status = DemandStatus.Posted, }; d.Status.Should().Be(DemandStatus.Posted); d.Lines.Should().BeEmpty(); d.Lines.Add(new DemandLine { ProductId = Guid.NewGuid(), Quantity = 1m, UnitPrice = 500m, LineTotal = 500m, VatPercent = 12m }); d.Lines.Should().HaveCount(1); } [Fact] public void Loyalty_Promotion_pocos() { var prog = new LoyaltyProgram { Name = "Plus", Type = LoyaltyProgramType.Percentage, Rate = 10m, IsActive = true }; prog.Name.Should().Be("Plus"); prog.Type.Should().Be(LoyaltyProgramType.Percentage); prog.Cards.Should().BeEmpty(); var card = new LoyaltyCard { CardNumber = "ABC-1", ProgramId = prog.Id, CounterpartyId = Guid.NewGuid(), Balance = 500m }; card.CardNumber.Should().Be("ABC-1"); card.Balance.Should().Be(500m); var promo = new Promotion { Name = "10%", Type = PromotionType.Percent, Value = 10m, IsActive = true, StartsAt = DateTime.UtcNow.AddDays(-1), EndsAt = DateTime.UtcNow.AddDays(30), }; promo.Name.Should().Be("10%"); promo.ProductGroupIds.Should().BeEmpty(); promo.ProductIds.Should().BeEmpty(); } // ── Purchases ───────────────────────────────────────────────────────── [Fact] public void Supply_and_SupplyLine_round_trip() { var s = new Supply { Number = "П-001", Date = DateTime.UtcNow, SupplierId = Guid.NewGuid(), StoreId = Guid.NewGuid(), CurrencyId = Guid.NewGuid(), Total = 1000m, Status = SupplyStatus.Posted, }; s.Status.Should().Be(SupplyStatus.Posted); s.Lines.Should().BeEmpty(); s.Lines.Add(new SupplyLine { ProductId = Guid.NewGuid(), Quantity = 10m, UnitPrice = 100m, LineTotal = 1000m }); s.Lines.Should().HaveCount(1); } [Fact] public void Enter_and_SupplierReturn_pocos() { var e = new Enter { Number = "ОПР-001", Date = DateTime.UtcNow, StoreId = Guid.NewGuid(), CurrencyId = Guid.NewGuid(), Status = EnterStatus.Posted, }; e.Lines.Should().BeEmpty(); e.Lines.Add(new EnterLine { ProductId = Guid.NewGuid(), Quantity = 1m }); var r = new SupplierReturn { Number = "ВП-001", Date = DateTime.UtcNow, SupplierId = Guid.NewGuid(), StoreId = Guid.NewGuid(), CurrencyId = Guid.NewGuid(), Status = SupplierReturnStatus.Posted, }; r.Lines.Should().BeEmpty(); r.Lines.Add(new SupplierReturnLine { ProductId = Guid.NewGuid(), Quantity = 1m, UnitPrice = 100m }); } // ── Inventory ───────────────────────────────────────────────────────── [Fact] public void Inventory_movement_loss_transfer_pocos() { var loss = new Loss { Number = "СП-001", Date = DateTime.UtcNow, StoreId = Guid.NewGuid(), CurrencyId = Guid.NewGuid(), Status = LossStatus.Posted, }; loss.Lines.Add(new LossLine { ProductId = Guid.NewGuid(), Quantity = 1m }); var t = new Transfer { Number = "ПР-001", Date = DateTime.UtcNow, FromStoreId = Guid.NewGuid(), ToStoreId = Guid.NewGuid(), Status = TransferStatus.Posted, }; t.Lines.Add(new TransferLine { ProductId = Guid.NewGuid(), Quantity = 1m }); var inv = new InventoryDoc { Number = "ИН-001", Date = DateTime.UtcNow, StoreId = Guid.NewGuid(), Status = InventoryStatus.Posted, }; inv.Lines.Add(new InventoryLine { ProductId = Guid.NewGuid(), BookQty = 10m, ActualQty = 9m, Diff = -1m }); loss.Status.Should().Be(LossStatus.Posted); t.Status.Should().Be(TransferStatus.Posted); inv.Status.Should().Be(InventoryStatus.Posted); } [Fact] public void Stock_and_StockMovement_pocos() { var s = new Stock { ProductId = Guid.NewGuid(), StoreId = Guid.NewGuid(), Quantity = 100m }; s.Quantity.Should().Be(100m); s.ReservedQuantity.Should().Be(0m); var m = new StockMovement { ProductId = Guid.NewGuid(), StoreId = Guid.NewGuid(), Type = MovementType.Supply, Quantity = 100m, OccurredAt = DateTime.UtcNow, UnitCost = 50m, }; m.Quantity.Should().Be(100m); m.Type.Should().Be(MovementType.Supply); } // ── Catalog ────────────────────────────────────────────────────────── [Fact] public void Product_defaults_collections() { var p = new Product { Name = "Хлеб" }; p.Name.Should().Be("Хлеб"); p.Prices.Should().BeEmpty(); p.Barcodes.Should().BeEmpty(); p.Images.Should().BeEmpty(); p.Prices.Add(new ProductPrice { PriceTypeId = Guid.NewGuid(), CurrencyId = Guid.NewGuid(), Amount = 100m }); p.Barcodes.Add(new ProductBarcode { Code = "1234567890123", Type = BarcodeType.Ean13, IsPrimary = true }); p.Images.Add(new ProductImage { Url = "/uploads/p.jpg", IsMain = true }); p.Prices.Should().HaveCount(1); p.Barcodes.Should().HaveCount(1); p.Images.Should().HaveCount(1); } [Fact] public void Catalog_referencs_pocos() { var c = new Counterparty { Name = "Поставщик", Type = CounterpartyType.LegalEntity, Bin = "12345" }; c.Name.Should().Be("Поставщик"); c.Type.Should().Be(CounterpartyType.LegalEntity); var u = new UnitOfMeasure { Code = "796", Name = "шт" }; u.Code.Should().Be("796"); u.IsActive.Should().BeTrue(); var st = new Store { Name = "Главный", IsMain = true }; st.IsMain.Should().BeTrue(); var rp = new RetailPoint { Name = "Касса 1", StoreId = Guid.NewGuid() }; rp.Name.Should().Be("Касса 1"); var pt = new PriceType { Name = "Розничная", IsRetail = true, IsSystem = true }; pt.IsRetail.Should().BeTrue(); pt.IsSystem.Should().BeTrue(); var cu = new Currency { Code = "KZT", Symbol = "₸", Name = "Тенге" }; cu.Code.Should().Be("KZT"); var co = new Country { Code = "KZ", Name = "Казахстан", VatRate = 12m }; co.VatRate.Should().Be(12m); var g = new ProductGroup { Name = "Хлеб и выпечка" }; g.Name.Should().Be("Хлеб и выпечка"); var ou = new OrgUnitOfMeasure { UnitOfMeasureId = u.Id }; ou.UnitOfMeasureId.Should().Be(u.Id); } // ── Organizations ──────────────────────────────────────────────────── [Fact] public void Organization_defaults() { var o = new Organization { Name = "FM" }; o.Name.Should().Be("FM"); o.IsActive.Should().BeTrue(); o.IsArchived.Should().BeFalse(); o.CountryCode.Should().Be("KZ"); o.FiscalProvider.Should().Be(0, "по дефолту None — Sprint 11 scaffolding"); o.ShowReferencePriceOnProduct.Should().BeTrue(); } [Fact] public void Employee_role_audit_pocos() { var e = new Employee { FirstName = "Иван", LastName = "Иванов", Email = "ivan@example.kz", IsActive = true, }; e.FirstName.Should().Be("Иван"); e.IsActive.Should().BeTrue(); e.RetailPointAssignments.Should().BeEmpty(); e.RetailPointAssignments.Add(new EmployeeRetailPointAssignment { RetailPointId = Guid.NewGuid() }); var role = new EmployeeRole { Name = "Кассир", Permissions = new RolePermissions() }; role.Name.Should().Be("Кассир"); role.Permissions.Should().NotBeNull(); var audit = new OrgAuditLog { Action = "create", EntityType = "Product", EntityId = Guid.NewGuid(), ChangesJson = "{}" }; audit.Action.Should().Be("create"); var sa = new SuperAdminAuditLog { ActionType = "ChangeOwner", Description = "x", Reason = "y", ChangesJson = "{}" }; sa.ActionType.Should().Be("ChangeOwner"); } // ── Platform + Integrations ────────────────────────────────────────── [Fact] public void PlatformSettings_defaults() { var ps = new PlatformSettings(); ps.SmtpHost.Should().BeNull(); ps.SmtpStartTls.Should().BeTrue("дефолт — STARTTLS, см. comments"); ps.SmtpUseSsl.Should().BeFalse(); } [Fact] public void ImportJob_defaults() { var j = new ImportJob { Kind = "moysklad", Status = ImportJobStatus.Running }; j.Kind.Should().Be("moysklad"); j.Status.Should().Be(ImportJobStatus.Running); j.Stage.Should().BeNull(); j.ErrorsJson.Should().Be("[]", "JSON-массив по дефолту"); } }