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
This commit is contained in:
parent
dd3c1c58c0
commit
9a3817dff0
|
|
@ -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<Func<ProductDto, GenericAttributeDto, bool>> 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<bool>('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<bool>('IsMeasurable')")]
|
||||
public bool IsMeasurable
|
||||
{
|
||||
get => GenericAttributes.GetValueOrDefault<bool>(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();
|
||||
//}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -23,11 +23,11 @@ public class StockTakingItem : MgStockTakingItem<StockTaking, ProductDto>
|
|||
[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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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<Shipping> Shippings { get; set; }
|
||||
public List<OrderDto> 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<OrderDto>();
|
||||
var toon = AcToonSerializer.SerializeTypeMetadata<OrderDto>(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");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test helper class to verify reference handling with shared object instances.
|
||||
/// </summary>
|
||||
public class TestContainerWithSharedRefs
|
||||
{
|
||||
public ProductDto? Product1 { get; set; }
|
||||
public ProductDto? Product2 { get; set; }
|
||||
}
|
||||
|
|
@ -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<GenericAttributeDto>, I
|
|||
switch (ParentDataItem)
|
||||
{
|
||||
case IProductDto:
|
||||
if (!hasContextIdParameter) ContextIds![ContextKeyGroupIndex] = "Product";
|
||||
if (!hasContextIdParameter) ContextIds![ContextKeyGroupIndex] = nameof(Product);
|
||||
|
||||
break;
|
||||
case IOrderDto:
|
||||
|
|
|
|||
Loading…
Reference in New Issue