From 9a3817dff0acff5be923e5d0d994affb6d599e5a Mon Sep 17 00:00:00 2001 From: Loretta Date: Fri, 16 Jan 2026 09:28:17 +0100 Subject: [PATCH] Improve docs, naming, and reference handling for Toon/DTOs - Use nameof(Product) for key group consistency and refactor safety - Expand ToonDescription attributes with business context and legacy notes - Add and update property-level documentation for measurement logic - Add tests for AcToonSerializer reference handling (@ref markers) - Introduce FullProcessModel and TestContainerWithSharedRefs for tests - Add DomainDescription constant for plugin documentation - Refine test output and assertions for metadata and navigation completeness - Minor property description and naming improvements throughout --- FruitBank.Common/Dtos/ProductDto.cs | 14 ++-- .../Entities/MeasuringItemPalletBase.cs | 10 ++- FruitBank.Common/Entities/OrderItemPallet.cs | 3 +- .../Entities/ShippingDocumentToFiles.cs | 2 + .../Entities/ShippingItemPallet.cs | 2 +- FruitBank.Common/Entities/StockTakingItem.cs | 4 +- .../Entities/StockTakingItemPallet.cs | 2 +- FruitBank.Common/FruitBankConstClient.cs | 2 +- FruitBankHybrid.Shared.Tests/ToonTests.cs | 73 +++++++++++++++++-- .../GridGenericAttributeBase.cs | 3 +- 10 files changed, 94 insertions(+), 21 deletions(-) diff --git a/FruitBank.Common/Dtos/ProductDto.cs b/FruitBank.Common/Dtos/ProductDto.cs index b95a661f..c531cc61 100644 --- a/FruitBank.Common/Dtos/ProductDto.cs +++ b/FruitBank.Common/Dtos/ProductDto.cs @@ -5,6 +5,8 @@ using Mango.Nop.Core.Dtos; using Mango.Nop.Core.Extensions; using Mango.Nop.Core.Interfaces.ForeignKeys; using Newtonsoft.Json; +using Nop.Core.Domain.Catalog; + //using Nop.Core.Domain.Catalog; using Nop.Core.Domain.Common; using Nop.Core.Domain.Orders; @@ -13,14 +15,14 @@ using System.Linq.Expressions; namespace FruitBank.Common.Dtos; -[LinqToDB.Mapping.Table(Name = "Product")] -[System.ComponentModel.DataAnnotations.Schema.Table("Product")] -[ToonDescription("Product data with measurements and generic attributes", TypeRelation = ToonTypeRelation.DtoOf)] +[LinqToDB.Mapping.Table(Name = nameof(Product))] +[System.ComponentModel.DataAnnotations.Schema.Table(nameof(Product))] +[ToonDescription("Product data with measurements and generic attributes", TypeRelation = ToonTypeRelation.DtoOf, RelatedTypes = [typeof(Product)])] public class ProductDto : MgProductDto, IProductDto { [NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore] private static Expression> RelationWithGenericAttribute => (orderItemDto, genericAttributeDto) => - orderItemDto.Id == genericAttributeDto.EntityId && genericAttributeDto.KeyGroup == "Product";// nameof(Product); + orderItemDto.Id == genericAttributeDto.EntityId && genericAttributeDto.KeyGroup == nameof(Product);// nameof(Product); [Association(ThisKey = nameof(Id), OtherKey = nameof(GenericAttributeDto.EntityId), ExpressionPredicate = nameof(RelationWithGenericAttribute), CanBeNull = false)] @@ -34,7 +36,7 @@ public class ProductDto : MgProductDto, IProductDto //{ } [NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore] - [ToonDescription(Purpose = "Status flag", BusinessRule = "get => GenericAttributes.GetValueOrDefault('IsMeasurable')")] + [ToonDescription(Purpose = "Master flag: if false, the system bypasses weight validation but still creates one Measurement Record (PalletItem) with TrayQuantity.", BusinessRule = "get => GenericAttributes.GetValueOrDefault('IsMeasurable')")] public bool IsMeasurable { get => GenericAttributes.GetValueOrDefault(nameof(IMeasurable.IsMeasurable)); @@ -74,7 +76,7 @@ public class ProductDto : MgProductDto, IProductDto //set //{ // var ga = GenericAttributes.FirstOrDefault(ga => ga.Key == nameof(IIncomingQuantity.IncomingQuantity)) ?? - // GenericAttributes.AddNewGenericAttribute("Product", nameof(IIncomingQuantity.IncomingQuantity), value.ToString(), Id); + // GenericAttributes.AddNewGenericAttribute(nameof(Product), nameof(IIncomingQuantity.IncomingQuantity), value.ToString(), Id); // ga.Value = value.ToString(); //} diff --git a/FruitBank.Common/Entities/MeasuringItemPalletBase.cs b/FruitBank.Common/Entities/MeasuringItemPalletBase.cs index 87236498..d38b3d0a 100644 --- a/FruitBank.Common/Entities/MeasuringItemPalletBase.cs +++ b/FruitBank.Common/Entities/MeasuringItemPalletBase.cs @@ -9,7 +9,8 @@ using Newtonsoft.Json; namespace FruitBank.Common.Entities; -[ToonDescription("Base class for pallet measurements with net weight calculation")] +[ToonDescription("Base class for pallet measurements with net weight calculation", + Purpose = "Technically named 'Pallet' for legacy reasons, but represents a General Measurement Record. It is ALWAYS created for every item. If the product is not measurable, weights are 0 and only TrayQuantity is used.")] public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPalletBase { private double _palletWeight; @@ -20,9 +21,10 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall protected int ForeignItemId; [NotColumn] - [ToonDescription(BusinessRule = "get => ForeignItemId")] + [ToonDescription(BusinessRule = "get => ForeignItemId", Constraints = "[#SmartTypeConstraints]")] public int ForeignKey => ForeignItemId; + [ToonDescription(Purpose = "Always recorded, regardless of measurability")] public int TrayQuantity { get; set; } [Column(DataType = DataType.DecFloat)] @@ -33,6 +35,7 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall } [Column(DataType = DataType.DecFloat)] + [ToonDescription(Purpose = "Weight of the physical pallet if used; 0.0 if goods arrive without a pallet")] public double PalletWeight { get => _palletWeight; @@ -40,7 +43,7 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall } [NotColumn, System.ComponentModel.DataAnnotations.Schema.NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore] - [ToonDescription(BusinessRule = "get => CalculateNetWeight()", Constraints = "[#SmartTypeConstraints], readonly")] + [ToonDescription(BusinessRule = "get => GrossWeight - PalletWeight - (TrayQuantity * TareWeight)", Constraints = "[#SmartTypeConstraints], readonly")] public double NetWeight { get => CalculateNetWeight(); @@ -48,6 +51,7 @@ public abstract class MeasuringItemPalletBase : MgEntityBase, IMeasuringItemPall } [Column(DataType = DataType.DecFloat, CanBeNull = false)] + [ToonDescription(Purpose = "Measured gross weight; 0.0 if product is not measurable")] public double GrossWeight { get => _grossWeight; diff --git a/FruitBank.Common/Entities/OrderItemPallet.cs b/FruitBank.Common/Entities/OrderItemPallet.cs index c87dd9d2..ef8a3116 100644 --- a/FruitBank.Common/Entities/OrderItemPallet.cs +++ b/FruitBank.Common/Entities/OrderItemPallet.cs @@ -11,7 +11,7 @@ using Table = LinqToDB.Mapping.TableAttribute; namespace FruitBank.Common.Entities; -[ToonDescription("Pallet measurements for order items with audit tracking", Purpose = "A measurement record for outgoing goods, used to verify that the net weight being sent to the customer is accurate and audited")] +[ToonDescription("Pallet measurements for order items with audit tracking", Purpose = "A measurement record for outgoing goods, used to verify that the net weight being sent to the customer is accurate and audited. NOTE: Despite the 'Pallet' name, this is a general measurement record that is ALWAYS created for every item. If the product is not measurable (IsMeasurable=false), weights are recorded as 0.0 and only TrayQuantity is stored.")] [Table(Name = FruitBankConstClient.OrderItemPalletDbTableName)] [System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.OrderItemPalletDbTableName)] public class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet @@ -22,6 +22,7 @@ public class OrderItemPallet : MeasuringItemPalletBase, IOrderItemPallet set => ForeignItemId = value; } + [ToonDescription(Purpose = "User/Customer ID of the quality auditor")] public int RevisorId { get; set; } [NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore] diff --git a/FruitBank.Common/Entities/ShippingDocumentToFiles.cs b/FruitBank.Common/Entities/ShippingDocumentToFiles.cs index c85c6c2a..e68c2b28 100644 --- a/FruitBank.Common/Entities/ShippingDocumentToFiles.cs +++ b/FruitBank.Common/Entities/ShippingDocumentToFiles.cs @@ -13,8 +13,10 @@ namespace FruitBank.Common.Entities; public class ShippingDocumentToFiles : MgEntityBase, IShippingDocumentToFiles { public int FilesId { get; set; } + public int ShippingDocumentId { get; set; } + [ToonDescription(Constraints = "enum-reference: DocumentType")] public int DocumentTypeId { get; set; } [NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore] diff --git a/FruitBank.Common/Entities/ShippingItemPallet.cs b/FruitBank.Common/Entities/ShippingItemPallet.cs index ba4e4b72..cdb7d2df 100644 --- a/FruitBank.Common/Entities/ShippingItemPallet.cs +++ b/FruitBank.Common/Entities/ShippingItemPallet.cs @@ -7,7 +7,7 @@ using System.Security.Cryptography.X509Certificates; namespace FruitBank.Common.Entities; -[ToonDescription("Pallet measurements for shipping items", Purpose = "The smallest unit of measurement tracking, representing a single physical pallet of a shipping item, used for precise gross-to-net weight calculation and quality audit")] +[ToonDescription("Pallet measurements for shipping items", Purpose = "The smallest unit of measurement tracking, representing a single physical measurement event. NOTE: Technically named 'Pallet' for legacy reasons, but it is ALWAYS created even if goods arrive without a physical pallet. For non-measurable products, weights are 0.0 and only TrayQuantity is tracked for tare-weight calculations.")] [LinqToDB.Mapping.Table(Name = FruitBankConstClient.ShippingItemPalletDbTableName)] [System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.ShippingItemPalletDbTableName)] public class ShippingItemPallet : MeasuringItemPalletBase, IShippingItemPallet diff --git a/FruitBank.Common/Entities/StockTakingItem.cs b/FruitBank.Common/Entities/StockTakingItem.cs index cc4d7189..4c7d894a 100644 --- a/FruitBank.Common/Entities/StockTakingItem.cs +++ b/FruitBank.Common/Entities/StockTakingItem.cs @@ -23,11 +23,11 @@ public class StockTakingItem : MgStockTakingItem [Column(DataType = DataType.DecFloat, CanBeNull = false)] public double MeasuredNetWeight { get; set; } - [ToonDescription(Purpose = "Reserved stock buffer to prevent double-deduction during closing")] + [ToonDescription(Purpose = "Reserved stock buffer (not yet shipped) to prevent double-deduction during closing")] public int InProcessOrdersQuantity { get; set; } [NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore] - [ToonDescription(BusinessRule = "get => OriginalStockQuantity + InProcessOrdersQuantity")] + [ToonDescription(BusinessRule = "get => OriginalStockQuantity + InProcessOrdersQuantity", Purpose = "Snapshot of total logical stock at session start")] public int TotalOriginalQuantity => OriginalStockQuantity + InProcessOrdersQuantity; [NotColumn, NotMapped, JsonIgnore, System.Text.Json.Serialization.JsonIgnore] diff --git a/FruitBank.Common/Entities/StockTakingItemPallet.cs b/FruitBank.Common/Entities/StockTakingItemPallet.cs index eb881783..24eed2d7 100644 --- a/FruitBank.Common/Entities/StockTakingItemPallet.cs +++ b/FruitBank.Common/Entities/StockTakingItemPallet.cs @@ -13,7 +13,7 @@ public interface IStockTakingItemPallet : IMeasuringItemPalletBase public StockTakingItem? StockTakingItem{ get; set; } } -[ToonDescription("Weight record for inventory item", Purpose = "Granular weight-based evidence for a stock taking line item")] +[ToonDescription("Weight record for inventory item", Purpose = "Granular weight-based evidence for a stock taking line item. NOTE: This record is mandatory for every inventory item. If weighing is skipped (non-measurable), it serves as a container for TrayQuantity with zeroed weight fields. The term 'Pallet' is a legacy naming convention.")] [LinqToDB.Mapping.Table(Name = FruitBankConstClient.StockTakingItemPalletDbTableName)] [System.ComponentModel.DataAnnotations.Schema.Table(FruitBankConstClient.StockTakingItemPalletDbTableName)] public class StockTakingItemPallet : MeasuringItemPalletBase, IStockTakingItemPallet diff --git a/FruitBank.Common/FruitBankConstClient.cs b/FruitBank.Common/FruitBankConstClient.cs index f4cbe29b..8d8471d2 100644 --- a/FruitBank.Common/FruitBankConstClient.cs +++ b/FruitBank.Common/FruitBankConstClient.cs @@ -43,8 +43,8 @@ public static class FruitBankConstClient public const string StockTakingDbTableName = "fbStockTaking"; public const string StockTakingItemDbTableName = "fbStockTakingItem"; public const string StockTakingItemPalletDbTableName = "fbStockTakingItemPallet"; - + public const string DomainDescription = "This is a nopCommerce plugin developed for FruitBank, a fruit and vegetable wholesaler. The plugin manages supplier inbound delivery (receiving), warehouse weighing (net/gross/pallet/tare weights), and inventory stocktaking. The business logic is centered around FruitBank's requirement for precise physical measurement and quantity tracking."; //public static Guid[] DevAdminIds = new Guid[2] { Guid.Parse("dcf451d2-cc4c-4ac2-8c1f-da00041be1fd"), Guid.Parse("4cbaed43-2465-4d99-84f1-c8bc6b7025f7") }; //public static Guid[] SysAdmins = new Guid[3] diff --git a/FruitBankHybrid.Shared.Tests/ToonTests.cs b/FruitBankHybrid.Shared.Tests/ToonTests.cs index 4f30b123..f56d4b35 100644 --- a/FruitBankHybrid.Shared.Tests/ToonTests.cs +++ b/FruitBankHybrid.Shared.Tests/ToonTests.cs @@ -18,6 +18,14 @@ using System.Runtime.Serialization; namespace FruitBankHybrid.Shared.Tests; +//[ToonIgnore][ToonDataIgnore] +[ToonDescription(Purpose = "Container model for Shipping, Order")] +public class FullProcessModel +{ + public List Shippings { get; set; } + public List Orders { get; set; } +} + [TestClass] public sealed class ToonTests { @@ -41,11 +49,57 @@ public sealed class ToonTests [TestMethod] public async Task OrderDtoToToon() { - var orderDtos = await _signalRClient.GetAllOrderDtos(); - var toon = AcToonSerializer.Serialize(orderDtos, AcToonSerializerOptions.Default); - + var a = new FullProcessModel(); + a.Orders = (await _signalRClient.GetAllOrderDtos())!; + a.Shippings = (await _signalRClient.GetShippings())!; + + var toon = AcToonSerializer.Serialize(a, FruitBankConstClient.DomainDescription, AcToonSerializerOptions.Default); + Console.WriteLine(toon); Assert.IsNotEmpty(toon); + // Note: @ref: only appears when the same object instance is referenced multiple times. + // Data from separate API calls typically don't share object instances. + } + + [TestMethod] + public void ReferenceHandling_WithSharedReferences_ShouldOutputRefMarkers() + { + // Create a simple test container with shared references + var sharedProduct = new ProductDto { Id = 1, Name = "Shared Product" }; + + // Create a container that references the same ProductDto twice + var container = new TestContainerWithSharedRefs + { + Product1 = sharedProduct, + Product2 = sharedProduct // Same instance, should create @ref + }; + + var toon = AcToonSerializer.Serialize(container, FruitBankConstClient.DomainDescription, AcToonSerializerOptions.Default); + + Console.WriteLine(toon); + Assert.IsNotEmpty(toon); + Assert.IsTrue(toon.Contains("@ref:"), "ReferenceHandling should detect shared Product instance"); + } + + [TestMethod] + public void ReferenceHandling_WithSharedIIdReferences_ShouldOutputRefMarkers() + { + // Create a simple test container with shared references + var sharedProduct = new ProductDto { Id = 1, Name = "Shared Product" }; + var sharedProduct2 = new ProductDto { Id = 1, Name = "Shared Product" }; + + // Create a container that references the same ProductDto twice + var container = new TestContainerWithSharedRefs + { + Product1 = sharedProduct, + Product2 = sharedProduct2 // Same instance, should create @ref + }; + + var toon = AcToonSerializer.Serialize(container, FruitBankConstClient.DomainDescription, AcToonSerializerOptions.Default); + + Console.WriteLine(toon); + Assert.IsNotEmpty(toon); + Assert.IsTrue(toon.Contains("@ref:"), "ReferenceHandling should detect shared Product instance"); } [TestMethod] @@ -94,7 +148,7 @@ public sealed class ToonTests [TestMethod] public void ToonTypes_PropertyDescriptions_ShouldNotBeRedundantOrMisleading() { - var toon = AcToonSerializer.SerializeTypeMetadata(); + var toon = AcToonSerializer.SerializeTypeMetadata(FruitBankConstClient.DomainDescription); var lines = toon.Split('\n'); foreach (var line in lines) { @@ -108,7 +162,7 @@ public sealed class ToonTests [TestMethod] public void ToonTypes_NavigationMetadata_ShouldBeComplete() { - var toon = AcToonSerializer.SerializeMetadata([typeof(Shipping), typeof(OrderDto), typeof(StockTaking), typeof(StockQuantityHistory), typeof(StockQuantityHistoryExt)]); + var toon = AcToonSerializer.SerializeMetadata(FruitBankConstClient.DomainDescription, [typeof(Shipping), typeof(OrderDto), typeof(StockTaking), typeof(StockQuantityHistory), typeof(StockQuantityHistoryExt)]); Console.WriteLine(toon); Console.WriteLine("\n=== NAVIGATION METADATA ELLENŐRZÉS ===\n"); @@ -308,4 +362,13 @@ public sealed class ToonTests Console.WriteLine("✓ Minden navigation property tartalmazza a szükséges metadatokat"); } +} + +/// +/// Test helper class to verify reference handling with shared object instances. +/// +public class TestContainerWithSharedRefs +{ + public ProductDto? Product1 { get; set; } + public ProductDto? Product2 { get; set; } } \ No newline at end of file diff --git a/FruitBankHybrid.Shared/Components/Grids/GenericAttributes/GridGenericAttributeBase.cs b/FruitBankHybrid.Shared/Components/Grids/GenericAttributes/GridGenericAttributeBase.cs index 9e3c4815..37907f95 100644 --- a/FruitBankHybrid.Shared/Components/Grids/GenericAttributes/GridGenericAttributeBase.cs +++ b/FruitBankHybrid.Shared/Components/Grids/GenericAttributes/GridGenericAttributeBase.cs @@ -7,6 +7,7 @@ using FruitBank.Common.Interfaces; using FruitBank.Common.SignalRs; using FruitBankHybrid.Shared.Pages; using Microsoft.AspNetCore.Components; +using Nop.Core.Domain.Catalog; using Nop.Core.Domain.Common; using Nop.Core.Domain.Orders; @@ -51,7 +52,7 @@ public class GridGenericAttributeBase: FruitBankGridBase, I switch (ParentDataItem) { case IProductDto: - if (!hasContextIdParameter) ContextIds![ContextKeyGroupIndex] = "Product"; + if (!hasContextIdParameter) ContextIds![ContextKeyGroupIndex] = nameof(Product); break; case IOrderDto: