diff --git a/src/Business/Grand.Business.Common/Services/Audit/AuditService.cs b/src/Business/Grand.Business.Common/Services/Audit/AuditService.cs index 088b215293..56edae1083 100644 --- a/src/Business/Grand.Business.Common/Services/Audit/AuditService.cs +++ b/src/Business/Grand.Business.Common/Services/Audit/AuditService.cs @@ -41,7 +41,7 @@ public async IAsyncEnumerable StreamEntries(AuditEntrySearch search, var bounded = new AuditEntrySearch { FromUtc = search.FromUtc, ToUtc = search.ToUtc == null || search.ToUtc > now ? now : search.ToUtc, - ActorEmail = search.ActorEmail, + SearchText = search.SearchText, Source = search.Source, Kind = search.Kind, EntityType = search.EntityType, @@ -108,10 +108,11 @@ private IQueryable Query(AuditEntrySearch search) query = query.Where(x => x.CreatedOnUtc >= search.FromUtc.Value); if (search.ToUtc.HasValue) query = query.Where(x => x.CreatedOnUtc <= search.ToUtc.Value); - if (!string.IsNullOrEmpty(search.ActorEmail)) + if (!string.IsNullOrEmpty(search.SearchText)) { - var email = search.ActorEmail.ToLowerInvariant(); - query = query.Where(x => x.ActorEmail != null && x.ActorEmail.ToLower().Contains(email)); + var text = search.SearchText.ToLowerInvariant(); + query = query.Where(x => (x.ActorEmail != null && x.ActorEmail.ToLower().Contains(text)) + || (x.EntityName != null && x.EntityName.ToLower().Contains(text))); } if (search.Source.HasValue) diff --git a/src/Business/Grand.Business.Core/Interfaces/Common/Audit/AuditEntrySearch.cs b/src/Business/Grand.Business.Core/Interfaces/Common/Audit/AuditEntrySearch.cs index 0069d0d5c0..f95305c5f2 100644 --- a/src/Business/Grand.Business.Core/Interfaces/Common/Audit/AuditEntrySearch.cs +++ b/src/Business/Grand.Business.Core/Interfaces/Common/Audit/AuditEntrySearch.cs @@ -9,7 +9,10 @@ public class AuditEntrySearch { public DateTime? FromUtc { get; set; } public DateTime? ToUtc { get; set; } - public string ActorEmail { get; set; } + /// + /// Part of the staff member's e-mail or of the record's name, any case + /// + public string SearchText { get; set; } public AuditSource? Source { get; set; } public AuditKind? Kind { get; set; } public string EntityType { get; set; } diff --git a/src/Core/Grand.Data/Audit/AuditSnapshot.cs b/src/Core/Grand.Data/Audit/AuditSnapshot.cs index aa16ef2f9b..ace63c2ee0 100644 --- a/src/Core/Grand.Data/Audit/AuditSnapshot.cs +++ b/src/Core/Grand.Data/Audit/AuditSnapshot.cs @@ -22,7 +22,12 @@ public static class AuditSnapshot public static IDictionary Take(T entity) where T : BaseEntity { - return Flatten(entity.ToBsonDocument(), typeof(T) == typeof(Setting)); + return Take(entity, typeof(T)); + } + + public static IDictionary Take(object entity, Type entityType) + { + return Flatten(entity.ToBsonDocument(entityType), entityType == typeof(Setting)); } public static IDictionary Flatten(BsonDocument document, bool isSetting) @@ -56,7 +61,13 @@ private static void Flatten(string path, BsonValue value, Dictionary v.IsBsonDocument || v.IsBsonArray): for (var i = 0; i < array.Count; i++) - Flatten($"{path}[{ElementKey(array[i], i)}]", array[i], leaves); + { + //the key of a user field is already in its path; repeating it as a field shows a change of nothing + var element = array[i] is BsonDocument document && IsKeyed(document) + ? new BsonDocument(document.Where(e => e.Name is not ("Key" or "StoreId"))) + : array[i]; + Flatten($"{path}[{ElementKey(array[i], i)}]", element, leaves); + } break; default: //a scalar, or a list of scalars kept as one value; when element ids repeat, the first wins @@ -69,13 +80,25 @@ private static string ElementKey(BsonValue element, int index) { if (element is not BsonDocument document) return index.ToString(); - if ((document.TryGetValue("_id", out var id) || document.TryGetValue("Id", out id)) && !id.IsBsonNull) + if (HasId(document, out var id)) return $"id={id}"; - if (!document.TryGetValue("Key", out var key) || !key.IsString || key.AsString == "") + if (!IsKeyed(document)) return index.ToString(); + var key = document["Key"].AsString; return document.TryGetValue("StoreId", out var storeId) && storeId.IsString && storeId.AsString != "" - ? $"{key.AsString}@{storeId.AsString}" - : key.AsString; + ? $"{key}@{storeId.AsString}" + : key; + } + + private static bool HasId(BsonDocument document, out BsonValue id) + { + return (document.TryGetValue("_id", out id) || document.TryGetValue("Id", out id)) && !id.IsBsonNull; + } + + //a key/value pair such as a user field: matched by its key (once per store) instead of its position + private static bool IsKeyed(BsonDocument document) + { + return !HasId(document, out _) && document.TryGetValue("Key", out var key) && key.IsString && key.AsString != ""; } private static BsonDocument TryParse(string json) @@ -94,10 +117,16 @@ private static string Format(BsonValue value) { if (value.IsString) return value.AsString; - if (value.IsBsonBinaryData) - return Binary; + //a Guid is stored as binary (CSharpLegacy, see MongoDBStartupBase) but is a value like any other + if (value is BsonBinaryData binary) + return binary.SubType switch { + BsonBinarySubType.UuidLegacy => GuidConverter.FromBytes(binary.Bytes, GuidRepresentation.CSharpLegacy).ToString(), + BsonBinarySubType.UuidStandard => GuidConverter.FromBytes(binary.Bytes, GuidRepresentation.Standard).ToString(), + _ => Binary + }; + //to the second: a form that posts a date back drops its milliseconds, which is no change to a person if (value.IsValidDateTime) - return value.ToUniversalTime().ToString("o"); + return value.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'"); if (value.IsBsonArray) return value.ToJson(); return value.ToString(); diff --git a/src/Core/Grand.Data/Audit/IAuditRecordReader.cs b/src/Core/Grand.Data/Audit/IAuditRecordReader.cs new file mode 100644 index 0000000000..9b73bb20db --- /dev/null +++ b/src/Core/Grand.Data/Audit/IAuditRecordReader.cs @@ -0,0 +1,14 @@ +namespace Grand.Data.Audit; + +/// +/// Reads one stored record as an audit snapshot, to name the record a changed field refers to +/// +public interface IAuditRecordReader +{ + /// + /// The record of whose (the id when null) is + /// , as "path → value", or null when there is none. With , + /// the snapshot may hold only that top-level field. + /// + Task> Read(Type entityType, string value, string key = null, string field = null); +} diff --git a/src/Core/Grand.Data/LiteDb/LiteDBAuditRecordReader.cs b/src/Core/Grand.Data/LiteDb/LiteDBAuditRecordReader.cs new file mode 100644 index 0000000000..6227f59c71 --- /dev/null +++ b/src/Core/Grand.Data/LiteDb/LiteDBAuditRecordReader.cs @@ -0,0 +1,25 @@ +using Grand.Data.Audit; +using LiteDB; + +namespace Grand.Data.LiteDb; + +/// +/// Resolves the database only when a record is read, so it can be registered before installation +/// +public class LiteDBAuditRecordReader(Func database) : IAuditRecordReader +{ + //the embedded database reads the whole document either way, so the field is not projected + public Task> Read(Type entityType, string value, string key = null, + string field = null) + { + var db = database(); + var collection = db.GetCollection(entityType.Name); + //a key other than the id may hold a number, stored as one + var document = key == null + ? collection.FindById(new BsonValue(value)) + : collection.FindOne(Query.EQ(key, int.TryParse(value, out var number) ? new BsonValue(number) : new BsonValue(value))); + //through the entity, so the snapshot has the same shape as on MongoDB + var entity = document == null ? null : db.Mapper.ToObject(entityType, document); + return Task.FromResult(entity == null ? null : AuditSnapshot.Take(entity, entityType)); + } +} diff --git a/src/Core/Grand.Data/Mongo/MongoAuditRecordReader.cs b/src/Core/Grand.Data/Mongo/MongoAuditRecordReader.cs new file mode 100644 index 0000000000..8e5580609e --- /dev/null +++ b/src/Core/Grand.Data/Mongo/MongoAuditRecordReader.cs @@ -0,0 +1,24 @@ +using Grand.Data.Audit; +using Grand.Domain.Configuration; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace Grand.Data.Mongo; + +/// +/// Resolves the database only when a record is read, so it can be registered before installation +/// +public class MongoAuditRecordReader(Func database) : IAuditRecordReader +{ + public async Task> Read(Type entityType, string value, string key = null, + string field = null) + { + //a key other than the id may hold a number, stored as one + BsonValue match = key != null && int.TryParse(value, out var number) ? number : value; + var find = database().GetCollection(entityType.Name).Find(new BsonDocument(key ?? "_id", match)); + var document = field == null + ? await find.FirstOrDefaultAsync() + : await find.Project(Builders.Projection.Include(field)).FirstOrDefaultAsync(); + return AuditSnapshot.Flatten(document, entityType == typeof(Setting)); + } +} diff --git a/src/Core/Grand.Domain/Audit/AuditReferenceAttribute.cs b/src/Core/Grand.Domain/Audit/AuditReferenceAttribute.cs new file mode 100644 index 0000000000..1e17ee6059 --- /dev/null +++ b/src/Core/Grand.Domain/Audit/AuditReferenceAttribute.cs @@ -0,0 +1,25 @@ +namespace Grand.Domain.Audit; + +/// +/// Marks a property that refers to another record (or holds an enum value as a number), so the audit log shows +/// "name (id)" instead of a bare id. is the referenced entity or the enum; +/// names the list inside the target that holds the referenced element, whose owning +/// target record is given by the sibling property named in . +/// +[AttributeUsage(AttributeTargets.Property)] +public sealed class AuditReferenceAttribute(Type target, string list = null) : Attribute +{ + public Type Target { get; } = target; + + public string List { get; } = list; + + /// + /// The sibling property holding the id of the target record that contains + /// + public string Owner { get; set; } + + /// + /// The property of the target that the value matches, when it is not the id (an order status number) + /// + public string Key { get; set; } +} diff --git a/src/Core/Grand.Domain/Blogs/BlogCategory.cs b/src/Core/Grand.Domain/Blogs/BlogCategory.cs index aefc7bede4..3a81e838a8 100644 --- a/src/Core/Grand.Domain/Blogs/BlogCategory.cs +++ b/src/Core/Grand.Domain/Blogs/BlogCategory.cs @@ -1,4 +1,5 @@ -using Grand.Domain.Localization; +using Grand.Domain.Audit; +using Grand.Domain.Localization; using Grand.Domain.Stores; namespace Grand.Domain.Blogs; @@ -27,6 +28,7 @@ public class BlogCategory : BaseEntity, IStoreLinkEntity, ITranslationEntity /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); /// diff --git a/src/Core/Grand.Domain/Blogs/BlogCategoryPost.cs b/src/Core/Grand.Domain/Blogs/BlogCategoryPost.cs index b0558e932b..4becd91a74 100644 --- a/src/Core/Grand.Domain/Blogs/BlogCategoryPost.cs +++ b/src/Core/Grand.Domain/Blogs/BlogCategoryPost.cs @@ -1,6 +1,9 @@ -namespace Grand.Domain.Blogs; +using Grand.Domain.Audit; + +namespace Grand.Domain.Blogs; public class BlogCategoryPost : SubBaseEntity { + [AuditReference(typeof(BlogPost))] public string BlogPostId { get; set; } } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Blogs/BlogComment.cs b/src/Core/Grand.Domain/Blogs/BlogComment.cs index d4d71be035..8d29ab01ac 100644 --- a/src/Core/Grand.Domain/Blogs/BlogComment.cs +++ b/src/Core/Grand.Domain/Blogs/BlogComment.cs @@ -1,3 +1,7 @@ +using Grand.Domain.Stores; +using Grand.Domain.Customers; +using Grand.Domain.Audit; + namespace Grand.Domain.Blogs; /// @@ -8,11 +12,13 @@ public class BlogComment : BaseEntity /// /// Gets or sets the customer identifier /// + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } /// /// Gets or sets the store identifier /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// @@ -28,5 +34,6 @@ public class BlogComment : BaseEntity /// /// Gets or sets the blog post identifier /// + [AuditReference(typeof(BlogPost))] public string BlogPostId { get; set; } } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Blogs/BlogPost.cs b/src/Core/Grand.Domain/Blogs/BlogPost.cs index 5d9e6547a6..73d76af216 100644 --- a/src/Core/Grand.Domain/Blogs/BlogPost.cs +++ b/src/Core/Grand.Domain/Blogs/BlogPost.cs @@ -84,6 +84,7 @@ public class BlogPost : BaseEntity, ISlugEntity, IStoreLinkEntity, ITranslationE /// public virtual bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); /// diff --git a/src/Core/Grand.Domain/Blogs/BlogProduct.cs b/src/Core/Grand.Domain/Blogs/BlogProduct.cs index d31b7000d3..a17d93d753 100644 --- a/src/Core/Grand.Domain/Blogs/BlogProduct.cs +++ b/src/Core/Grand.Domain/Blogs/BlogProduct.cs @@ -1,7 +1,10 @@ -namespace Grand.Domain.Blogs; +using Grand.Domain.Audit; + +namespace Grand.Domain.Blogs; public class BlogProduct : BaseEntity { + [AuditReference(typeof(BlogPost))] public string BlogPostId { get; set; } public string ProductId { get; set; } public int DisplayOrder { get; set; } diff --git a/src/Core/Grand.Domain/Catalog/Bid.cs b/src/Core/Grand.Domain/Catalog/Bid.cs index 37fc9ed717..d5954ee9d8 100644 --- a/src/Core/Grand.Domain/Catalog/Bid.cs +++ b/src/Core/Grand.Domain/Catalog/Bid.cs @@ -1,4 +1,10 @@ -namespace Grand.Domain.Catalog; +using Grand.Domain.Shipping; +using Grand.Domain.Stores; +using Grand.Domain.Orders; +using Grand.Domain.Customers; +using Grand.Domain.Audit; + +namespace Grand.Domain.Catalog; /// /// Represents a auction bid @@ -8,11 +14,13 @@ public class Bid : BaseEntity /// /// Gets or sets the product identifier /// + [AuditReference(typeof(Product))] public string ProductId { get; set; } /// /// Gets or sets the warehouse identifier /// + [AuditReference(typeof(Warehouse))] public string WarehouseId { get; set; } /// @@ -23,6 +31,7 @@ public class Bid : BaseEntity /// /// Gets or sets the customer id /// + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } /// @@ -33,12 +42,14 @@ public class Bid : BaseEntity /// /// Gets or sets Order Id /// + [AuditReference(typeof(Order))] public string OrderId { get; set; } /// /// / /// Gets or sets Store Id /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// diff --git a/src/Core/Grand.Domain/Catalog/Brand.cs b/src/Core/Grand.Domain/Catalog/Brand.cs index 4c34ce0984..cfeb56c1d6 100644 --- a/src/Core/Grand.Domain/Catalog/Brand.cs +++ b/src/Core/Grand.Domain/Catalog/Brand.cs @@ -1,3 +1,6 @@ +using Grand.Domain.Customers; +using Grand.Domain.Discounts; +using Grand.Domain.Audit; using Grand.Domain.Localization; using Grand.Domain.Permissions; using Grand.Domain.Seo; @@ -30,6 +33,7 @@ public class Brand : BaseEntity, ITranslationEntity, ISlugEntity, IGroupLinkEnti /// /// Gets or sets a value of used brand layout identifier /// + [AuditReference(typeof(BrandLayout))] public string BrandLayoutId { get; set; } /// @@ -105,6 +109,7 @@ public class Brand : BaseEntity, ITranslationEntity, ISlugEntity, IGroupLinkEnti /// /// Gets or sets the collection of applied discounts /// + [AuditReference(typeof(Discount))] public virtual ICollection AppliedDiscounts { get => _appliedDiscounts ??= new List(); protected set => _appliedDiscounts = value; @@ -115,6 +120,7 @@ public virtual ICollection AppliedDiscounts { /// public bool LimitedToGroups { get; set; } + [AuditReference(typeof(CustomerGroup))] public IList CustomerGroups { get; set; } = new List(); /// @@ -127,6 +133,7 @@ public virtual ICollection AppliedDiscounts { /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); /// diff --git a/src/Core/Grand.Domain/Catalog/BundleProduct.cs b/src/Core/Grand.Domain/Catalog/BundleProduct.cs index 23c7397cae..67bcdb4a80 100644 --- a/src/Core/Grand.Domain/Catalog/BundleProduct.cs +++ b/src/Core/Grand.Domain/Catalog/BundleProduct.cs @@ -1,3 +1,5 @@ +using Grand.Domain.Audit; + namespace Grand.Domain.Catalog; /// @@ -8,6 +10,7 @@ public class BundleProduct : SubBaseEntity /// /// Gets or sets the product identifier /// + [AuditReference(typeof(Product))] public string ProductId { get; set; } /// diff --git a/src/Core/Grand.Domain/Catalog/Category.cs b/src/Core/Grand.Domain/Catalog/Category.cs index e84eb54bc6..62dc1d6b0c 100644 --- a/src/Core/Grand.Domain/Catalog/Category.cs +++ b/src/Core/Grand.Domain/Catalog/Category.cs @@ -1,3 +1,6 @@ +using Grand.Domain.Discounts; +using Grand.Domain.Customers; +using Grand.Domain.Audit; using Grand.Domain.Localization; using Grand.Domain.Permissions; using Grand.Domain.Seo; @@ -31,6 +34,7 @@ public class Category : BaseEntity, ISlugEntity, IGroupLinkEntity, ITranslationE /// /// Gets or sets a value of used category layout identifier /// + [AuditReference(typeof(CategoryLayout))] public string CategoryLayoutId { get; set; } /// @@ -51,6 +55,7 @@ public class Category : BaseEntity, ISlugEntity, IGroupLinkEntity, ITranslationE /// /// Gets or sets the parent category identifier /// + [AuditReference(typeof(Category))] public string ParentCategoryId { get; set; } /// @@ -141,6 +146,7 @@ public class Category : BaseEntity, ISlugEntity, IGroupLinkEntity, ITranslationE /// /// Gets or sets the collection of applied discounts /// + [AuditReference(typeof(Discount))] public virtual ICollection AppliedDiscounts { get => _appliedDiscounts ??= new List(); protected set => _appliedDiscounts = value; @@ -151,6 +157,7 @@ public virtual ICollection AppliedDiscounts { /// public bool LimitedToGroups { get; set; } + [AuditReference(typeof(CustomerGroup))] public IList CustomerGroups { get; set; } = new List(); /// @@ -163,6 +170,7 @@ public virtual ICollection AppliedDiscounts { /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); /// diff --git a/src/Core/Grand.Domain/Catalog/Collection.cs b/src/Core/Grand.Domain/Catalog/Collection.cs index e341d25fd3..5cbda39ada 100644 --- a/src/Core/Grand.Domain/Catalog/Collection.cs +++ b/src/Core/Grand.Domain/Catalog/Collection.cs @@ -1,3 +1,6 @@ +using Grand.Domain.Customers; +using Grand.Domain.Discounts; +using Grand.Domain.Audit; using Grand.Domain.Localization; using Grand.Domain.Permissions; using Grand.Domain.Seo; @@ -30,6 +33,7 @@ public class Collection : BaseEntity, ITranslationEntity, ISlugEntity, IGroupLin /// /// Gets or sets a value of used collection layout identifier /// + [AuditReference(typeof(CollectionLayout))] public string CollectionLayoutId { get; set; } /// @@ -110,6 +114,7 @@ public class Collection : BaseEntity, ITranslationEntity, ISlugEntity, IGroupLin /// /// Gets or sets the collection of applied discounts /// + [AuditReference(typeof(Discount))] public virtual ICollection AppliedDiscounts { get => _appliedDiscounts ??= new List(); protected set => _appliedDiscounts = value; @@ -120,6 +125,7 @@ public virtual ICollection AppliedDiscounts { /// public bool LimitedToGroups { get; set; } + [AuditReference(typeof(CustomerGroup))] public IList CustomerGroups { get; set; } = new List(); /// @@ -132,6 +138,7 @@ public virtual ICollection AppliedDiscounts { /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); /// diff --git a/src/Core/Grand.Domain/Catalog/OutOfStockSubscription.cs b/src/Core/Grand.Domain/Catalog/OutOfStockSubscription.cs index f51106d137..3a347cca8a 100644 --- a/src/Core/Grand.Domain/Catalog/OutOfStockSubscription.cs +++ b/src/Core/Grand.Domain/Catalog/OutOfStockSubscription.cs @@ -1,3 +1,7 @@ +using Grand.Domain.Shipping; +using Grand.Domain.Stores; +using Grand.Domain.Customers; +using Grand.Domain.Audit; using Grand.Domain.Common; namespace Grand.Domain.Catalog; @@ -10,11 +14,13 @@ public class OutOfStockSubscription : BaseEntity /// /// Gets or sets the store identifier /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// /// Gets or sets the product identifier /// + [AuditReference(typeof(Product))] public string ProductId { get; set; } /// @@ -30,10 +36,12 @@ public class OutOfStockSubscription : BaseEntity /// /// Gets or sets the warehouse identifier /// + [AuditReference(typeof(Warehouse))] public string WarehouseId { get; set; } /// /// Gets or sets the customer identifier /// + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Catalog/Product.cs b/src/Core/Grand.Domain/Catalog/Product.cs index b09f25fbd6..a8a4c0403a 100644 --- a/src/Core/Grand.Domain/Catalog/Product.cs +++ b/src/Core/Grand.Domain/Catalog/Product.cs @@ -1,3 +1,11 @@ +using Grand.Domain.Courses; +using Grand.Domain.Directory; +using Grand.Domain.Discounts; +using Grand.Domain.Vendors; +using Grand.Domain.Tax; +using Grand.Domain.Shipping; +using Grand.Domain.Customers; +using Grand.Domain.Audit; using Grand.Domain.Common; using Grand.Domain.Localization; using Grand.Domain.Permissions; @@ -37,6 +45,7 @@ public class Product : BaseEntity, ITranslationEntity, ISlugEntity, IGroupLinkEn /// Gets or sets the parent product identifier. It's used to identify associated products (only with "grouped" /// products) /// + [AuditReference(typeof(Product))] public string ParentGroupedProductId { get; set; } /// @@ -69,16 +78,19 @@ public class Product : BaseEntity, ITranslationEntity, ISlugEntity, IGroupLinkEn /// /// Gets or sets a value of used product layout identifier /// + [AuditReference(typeof(ProductLayout))] public string ProductLayoutId { get; set; } /// /// Gets or sets a brand identifier /// + [AuditReference(typeof(Brand))] public string BrandId { get; set; } /// /// Gets or sets a vendor identifier /// + [AuditReference(typeof(Vendor))] public string VendorId { get; set; } /// @@ -297,6 +309,7 @@ public class Product : BaseEntity, ITranslationEntity, ISlugEntity, IGroupLinkEn /// /// Gets or sets a delivery date identifier /// + [AuditReference(typeof(DeliveryDate))] public string DeliveryDateId { get; set; } /// @@ -307,6 +320,7 @@ public class Product : BaseEntity, ITranslationEntity, ISlugEntity, IGroupLinkEn /// /// Gets or sets the tax category identifier /// + [AuditReference(typeof(TaxCategory))] public string TaxCategoryId { get; set; } /// @@ -327,6 +341,7 @@ public class Product : BaseEntity, ITranslationEntity, ISlugEntity, IGroupLinkEn /// /// Gets or sets a warehouse identifier /// + [AuditReference(typeof(Warehouse))] public string WarehouseId { get; set; } /// @@ -473,6 +488,7 @@ public class Product : BaseEntity, ITranslationEntity, ISlugEntity, IGroupLinkEn /// /// Gets or sets a unit of product for PAngV (MeasureWeight entity) /// + [AuditReference(typeof(MeasureWeight))] public string BasepriceUnitId { get; set; } /// @@ -483,16 +499,19 @@ public class Product : BaseEntity, ITranslationEntity, ISlugEntity, IGroupLinkEn /// /// Gets or sets a reference unit for PAngV (MeasureWeight entity) /// + [AuditReference(typeof(MeasureWeight))] public string BasepriceBaseUnitId { get; set; } /// /// Gets or sets a unit of product /// + [AuditReference(typeof(MeasureUnit))] public string UnitId { get; set; } /// /// Gets or sets a course ident /// + [AuditReference(typeof(Course))] public string CourseId { get; set; } /// @@ -697,6 +716,7 @@ public virtual ICollection TierPrices { /// /// Gets or sets the collection of applied discounts /// + [AuditReference(typeof(Discount))] public virtual ICollection AppliedDiscounts { get => _appliedDiscounts ??= new List(); protected set => _appliedDiscounts = value; @@ -741,6 +761,7 @@ public virtual ICollection BundleProducts { /// public bool LimitedToGroups { get; set; } + [AuditReference(typeof(CustomerGroup))] public IList CustomerGroups { get; set; } = new List(); /// @@ -753,6 +774,7 @@ public virtual ICollection BundleProducts { /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); /// diff --git a/src/Core/Grand.Domain/Catalog/ProductAttribute.cs b/src/Core/Grand.Domain/Catalog/ProductAttribute.cs index 4c6a0b9423..6d012bdabd 100644 --- a/src/Core/Grand.Domain/Catalog/ProductAttribute.cs +++ b/src/Core/Grand.Domain/Catalog/ProductAttribute.cs @@ -1,3 +1,4 @@ +using Grand.Domain.Audit; using Grand.Domain.Localization; using Grand.Domain.Stores; @@ -31,6 +32,7 @@ public class ProductAttribute : BaseEntity, IStoreLinkEntity, ITranslationEntity /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); /// diff --git a/src/Core/Grand.Domain/Catalog/ProductAttributeMapping.cs b/src/Core/Grand.Domain/Catalog/ProductAttributeMapping.cs index 44fcce3337..1b2e2aea16 100644 --- a/src/Core/Grand.Domain/Catalog/ProductAttributeMapping.cs +++ b/src/Core/Grand.Domain/Catalog/ProductAttributeMapping.cs @@ -1,3 +1,4 @@ +using Grand.Domain.Audit; using Grand.Domain.Common; using Grand.Domain.Localization; @@ -13,6 +14,7 @@ public class ProductAttributeMapping : SubBaseEntity, ITranslationEntity /// /// Gets or sets the product attribute identifier /// + [AuditReference(typeof(ProductAttribute))] public string ProductAttributeId { get; set; } /// diff --git a/src/Core/Grand.Domain/Catalog/ProductAttributeValue.cs b/src/Core/Grand.Domain/Catalog/ProductAttributeValue.cs index 8e36dd72a0..bff5f0ddbf 100644 --- a/src/Core/Grand.Domain/Catalog/ProductAttributeValue.cs +++ b/src/Core/Grand.Domain/Catalog/ProductAttributeValue.cs @@ -1,3 +1,4 @@ +using Grand.Domain.Audit; using Grand.Domain.Localization; namespace Grand.Domain.Catalog; @@ -15,6 +16,7 @@ public class ProductAttributeValue : SubBaseEntity, ITranslationEntity /// /// Gets or sets the associated product identifier (used only with AttributeValueType.AssociatedToProduct) /// + [AuditReference(typeof(Product))] public string AssociatedProductId { get; set; } /// diff --git a/src/Core/Grand.Domain/Catalog/ProductCategory.cs b/src/Core/Grand.Domain/Catalog/ProductCategory.cs index c37d085572..acd3b14bab 100644 --- a/src/Core/Grand.Domain/Catalog/ProductCategory.cs +++ b/src/Core/Grand.Domain/Catalog/ProductCategory.cs @@ -1,3 +1,5 @@ +using Grand.Domain.Audit; + namespace Grand.Domain.Catalog; /// @@ -8,6 +10,7 @@ public class ProductCategory : SubBaseEntity /// /// Gets or sets the category identifier /// + [AuditReference(typeof(Category))] public string CategoryId { get; set; } /// diff --git a/src/Core/Grand.Domain/Catalog/ProductCollection.cs b/src/Core/Grand.Domain/Catalog/ProductCollection.cs index c4de7079c8..6bdaa9446e 100644 --- a/src/Core/Grand.Domain/Catalog/ProductCollection.cs +++ b/src/Core/Grand.Domain/Catalog/ProductCollection.cs @@ -1,3 +1,5 @@ +using Grand.Domain.Audit; + namespace Grand.Domain.Catalog; /// @@ -8,6 +10,7 @@ public class ProductCollection : SubBaseEntity /// /// Gets or sets the collection identifier /// + [AuditReference(typeof(Collection))] public string CollectionId { get; set; } /// diff --git a/src/Core/Grand.Domain/Catalog/ProductCombinationTierPrices.cs b/src/Core/Grand.Domain/Catalog/ProductCombinationTierPrices.cs index 6568837956..83fa0ddccf 100644 --- a/src/Core/Grand.Domain/Catalog/ProductCombinationTierPrices.cs +++ b/src/Core/Grand.Domain/Catalog/ProductCombinationTierPrices.cs @@ -1,3 +1,7 @@ +using Grand.Domain.Stores; +using Grand.Domain.Customers; +using Grand.Domain.Audit; + namespace Grand.Domain.Catalog; /// @@ -8,11 +12,13 @@ public class ProductCombinationTierPrices : SubBaseEntity /// /// Gets or sets the store identifier /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// /// Gets or sets the customer group identifier /// + [AuditReference(typeof(CustomerGroup))] public string CustomerGroupId { get; set; } /// diff --git a/src/Core/Grand.Domain/Catalog/ProductCombinationWarehouseInventory.cs b/src/Core/Grand.Domain/Catalog/ProductCombinationWarehouseInventory.cs index d25fe836b0..8e341df2c9 100644 --- a/src/Core/Grand.Domain/Catalog/ProductCombinationWarehouseInventory.cs +++ b/src/Core/Grand.Domain/Catalog/ProductCombinationWarehouseInventory.cs @@ -1,3 +1,6 @@ +using Grand.Domain.Shipping; +using Grand.Domain.Audit; + namespace Grand.Domain.Catalog; /// @@ -8,6 +11,7 @@ public class ProductCombinationWarehouseInventory : SubBaseEntity /// /// Gets or sets the warehouse identifier /// + [AuditReference(typeof(Warehouse))] public string WarehouseId { get; set; } /// diff --git a/src/Core/Grand.Domain/Catalog/ProductPrice.cs b/src/Core/Grand.Domain/Catalog/ProductPrice.cs index 86fed1ca45..5b8bba819e 100644 --- a/src/Core/Grand.Domain/Catalog/ProductPrice.cs +++ b/src/Core/Grand.Domain/Catalog/ProductPrice.cs @@ -1,7 +1,10 @@ -namespace Grand.Domain.Catalog; +using Grand.Domain.Audit; + +namespace Grand.Domain.Catalog; public class ProductPrice : SubBaseEntity { + [AuditReference(typeof(Product))] public string ProductId { get; set; } public string CurrencyCode { get; set; } public double Price { get; set; } diff --git a/src/Core/Grand.Domain/Catalog/ProductReservation.cs b/src/Core/Grand.Domain/Catalog/ProductReservation.cs index c6fe0a2034..0bb9602eb0 100644 --- a/src/Core/Grand.Domain/Catalog/ProductReservation.cs +++ b/src/Core/Grand.Domain/Catalog/ProductReservation.cs @@ -1,4 +1,7 @@ -namespace Grand.Domain.Catalog; +using Grand.Domain.Orders; +using Grand.Domain.Audit; + +namespace Grand.Domain.Catalog; /// /// Represents a product reservation @@ -8,6 +11,7 @@ public class ProductReservation : BaseEntity /// /// Gets or sets the product identifier /// + [AuditReference(typeof(Product))] public string ProductId { get; set; } /// @@ -18,6 +22,7 @@ public class ProductReservation : BaseEntity /// /// Gets or sets the order id /// + [AuditReference(typeof(Order))] public string OrderId { get; set; } /// diff --git a/src/Core/Grand.Domain/Catalog/ProductReview.cs b/src/Core/Grand.Domain/Catalog/ProductReview.cs index 879b0a050e..cf81b9f3f6 100644 --- a/src/Core/Grand.Domain/Catalog/ProductReview.cs +++ b/src/Core/Grand.Domain/Catalog/ProductReview.cs @@ -1,3 +1,5 @@ +using Grand.Domain.Stores; +using Grand.Domain.Customers; using Grand.Domain.Audit; namespace Grand.Domain.Catalog; @@ -12,16 +14,19 @@ public class ProductReview : BaseEntity /// /// Gets or sets the customer identifier /// + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } /// /// Gets or sets the product identifier /// + [AuditReference(typeof(Product))] public string ProductId { get; set; } /// /// Gets or sets the store identifier /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// diff --git a/src/Core/Grand.Domain/Catalog/ProductSpecificationAttribute.cs b/src/Core/Grand.Domain/Catalog/ProductSpecificationAttribute.cs index 48fd273b6f..195e610c4b 100644 --- a/src/Core/Grand.Domain/Catalog/ProductSpecificationAttribute.cs +++ b/src/Core/Grand.Domain/Catalog/ProductSpecificationAttribute.cs @@ -1,3 +1,5 @@ +using Grand.Domain.Audit; + namespace Grand.Domain.Catalog; /// @@ -13,11 +15,13 @@ public class ProductSpecificationAttribute : SubBaseEntity /// /// Gets or sets the specification attribute Id /// + [AuditReference(typeof(SpecificationAttribute))] public string SpecificationAttributeId { get; set; } /// /// Gets or sets the specification attribute identifier /// + [AuditReference(typeof(SpecificationAttribute), nameof(SpecificationAttribute.SpecificationAttributeOptions), Owner = nameof(SpecificationAttributeId))] public string SpecificationAttributeOptionId { get; set; } /// diff --git a/src/Core/Grand.Domain/Catalog/ProductWarehouseInventory.cs b/src/Core/Grand.Domain/Catalog/ProductWarehouseInventory.cs index 42873db5fd..206111ccae 100644 --- a/src/Core/Grand.Domain/Catalog/ProductWarehouseInventory.cs +++ b/src/Core/Grand.Domain/Catalog/ProductWarehouseInventory.cs @@ -1,3 +1,6 @@ +using Grand.Domain.Shipping; +using Grand.Domain.Audit; + namespace Grand.Domain.Catalog; /// @@ -8,6 +11,7 @@ public class ProductWarehouseInventory : SubBaseEntity /// /// Gets or sets the warehouse identifier /// + [AuditReference(typeof(Warehouse))] public string WarehouseId { get; set; } /// diff --git a/src/Core/Grand.Domain/Catalog/SpecificationAttribute.cs b/src/Core/Grand.Domain/Catalog/SpecificationAttribute.cs index fb5b46e233..e669ab8b72 100644 --- a/src/Core/Grand.Domain/Catalog/SpecificationAttribute.cs +++ b/src/Core/Grand.Domain/Catalog/SpecificationAttribute.cs @@ -1,3 +1,4 @@ +using Grand.Domain.Audit; using Grand.Domain.Localization; using Grand.Domain.Stores; @@ -38,6 +39,7 @@ public virtual ICollection SpecificationAttributeO /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); /// diff --git a/src/Core/Grand.Domain/Catalog/TierPrice.cs b/src/Core/Grand.Domain/Catalog/TierPrice.cs index 4dde028073..32d0cf0310 100644 --- a/src/Core/Grand.Domain/Catalog/TierPrice.cs +++ b/src/Core/Grand.Domain/Catalog/TierPrice.cs @@ -1,3 +1,7 @@ +using Grand.Domain.Stores; +using Grand.Domain.Customers; +using Grand.Domain.Audit; + namespace Grand.Domain.Catalog; /// @@ -8,11 +12,13 @@ public class TierPrice : SubBaseEntity /// /// Gets or sets the store identifier (0 - all stores) /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// /// Gets or sets the customer group identifier /// + [AuditReference(typeof(CustomerGroup))] public string CustomerGroupId { get; set; } /// diff --git a/src/Core/Grand.Domain/Common/Address.cs b/src/Core/Grand.Domain/Common/Address.cs index 585288d1f6..002ffe3921 100644 --- a/src/Core/Grand.Domain/Common/Address.cs +++ b/src/Core/Grand.Domain/Common/Address.cs @@ -1,4 +1,7 @@ -namespace Grand.Domain.Common; +using Grand.Domain.Directory; +using Grand.Domain.Audit; + +namespace Grand.Domain.Common; public class Address : SubBaseEntity { @@ -35,11 +38,13 @@ public class Address : SubBaseEntity /// /// Gets or sets the country identifier /// + [AuditReference(typeof(Country))] public string CountryId { get; set; } /// /// Gets or sets the state/province identifier /// + [AuditReference(typeof(Country), nameof(Country.StateProvinces), Owner = nameof(CountryId))] public string StateProvinceId { get; set; } /// diff --git a/src/Core/Grand.Domain/Common/AddressAttribute.cs b/src/Core/Grand.Domain/Common/AddressAttribute.cs index 3ae14065cc..d088b62873 100644 --- a/src/Core/Grand.Domain/Common/AddressAttribute.cs +++ b/src/Core/Grand.Domain/Common/AddressAttribute.cs @@ -1,3 +1,4 @@ +using Grand.Domain.Audit; using Grand.Domain.Catalog; using Grand.Domain.Localization; using Grand.Domain.Stores; @@ -58,5 +59,6 @@ public virtual ICollection AddressAttributeValues { /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Common/AddressAttributeValue.cs b/src/Core/Grand.Domain/Common/AddressAttributeValue.cs index 228485eb21..0bc5869b01 100644 --- a/src/Core/Grand.Domain/Common/AddressAttributeValue.cs +++ b/src/Core/Grand.Domain/Common/AddressAttributeValue.cs @@ -1,3 +1,4 @@ +using Grand.Domain.Audit; using Grand.Domain.Localization; namespace Grand.Domain.Common; @@ -10,6 +11,7 @@ public class AddressAttributeValue : BaseEntity, ITranslationEntity /// /// Gets or sets the address attribute identifier /// + [AuditReference(typeof(AddressAttribute))] public string AddressAttributeId { get; set; } /// diff --git a/src/Core/Grand.Domain/Courses/Course.cs b/src/Core/Grand.Domain/Courses/Course.cs index 0c3362169a..0065c961b1 100644 --- a/src/Core/Grand.Domain/Courses/Course.cs +++ b/src/Core/Grand.Domain/Courses/Course.cs @@ -1,4 +1,7 @@ -using Grand.Domain.Localization; +using Grand.Domain.Catalog; +using Grand.Domain.Customers; +using Grand.Domain.Audit; +using Grand.Domain.Localization; using Grand.Domain.Permissions; using Grand.Domain.Seo; using Grand.Domain.Stores; @@ -40,11 +43,13 @@ public class Course : BaseEntity, ITranslationEntity, ISlugEntity, IGroupLinkEnt /// /// Gets or sets the product ident /// + [AuditReference(typeof(Product))] public string ProductId { get; set; } /// /// Gets or sets the level ident /// + [AuditReference(typeof(CourseLevel))] public string LevelId { get; set; } /// @@ -67,6 +72,7 @@ public class Course : BaseEntity, ITranslationEntity, ISlugEntity, IGroupLinkEnt /// public bool LimitedToGroups { get; set; } + [AuditReference(typeof(CustomerGroup))] public IList CustomerGroups { get; set; } = new List(); /// @@ -79,6 +85,7 @@ public class Course : BaseEntity, ITranslationEntity, ISlugEntity, IGroupLinkEnt /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); /// diff --git a/src/Core/Grand.Domain/Courses/CourseAction.cs b/src/Core/Grand.Domain/Courses/CourseAction.cs index b13ca37092..f746fb131c 100644 --- a/src/Core/Grand.Domain/Courses/CourseAction.cs +++ b/src/Core/Grand.Domain/Courses/CourseAction.cs @@ -1,20 +1,26 @@ -namespace Grand.Domain.Courses; +using Grand.Domain.Customers; +using Grand.Domain.Audit; + +namespace Grand.Domain.Courses; public class CourseAction : BaseEntity { /// /// Gets or sets the customer ident /// + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } /// /// Gets or sets the course ident /// + [AuditReference(typeof(Course))] public string CourseId { get; set; } /// /// Gets or sets the lesson ident /// + [AuditReference(typeof(CourseLesson))] public string LessonId { get; set; } /// diff --git a/src/Core/Grand.Domain/Courses/CourseLesson.cs b/src/Core/Grand.Domain/Courses/CourseLesson.cs index 17bc14c870..7364808cd6 100644 --- a/src/Core/Grand.Domain/Courses/CourseLesson.cs +++ b/src/Core/Grand.Domain/Courses/CourseLesson.cs @@ -1,4 +1,6 @@ -namespace Grand.Domain.Courses; +using Grand.Domain.Audit; + +namespace Grand.Domain.Courses; public class CourseLesson : BaseEntity { @@ -25,11 +27,13 @@ public class CourseLesson : BaseEntity /// /// Gets or sets the course ident /// + [AuditReference(typeof(Course))] public string CourseId { get; set; } /// /// Gets or sets the subject ident /// + [AuditReference(typeof(CourseSubject))] public string SubjectId { get; set; } /// diff --git a/src/Core/Grand.Domain/Courses/CourseSubject.cs b/src/Core/Grand.Domain/Courses/CourseSubject.cs index d1e41744b4..0cf710aec3 100644 --- a/src/Core/Grand.Domain/Courses/CourseSubject.cs +++ b/src/Core/Grand.Domain/Courses/CourseSubject.cs @@ -1,4 +1,6 @@ -namespace Grand.Domain.Courses; +using Grand.Domain.Audit; + +namespace Grand.Domain.Courses; public class CourseSubject : BaseEntity { @@ -15,5 +17,6 @@ public class CourseSubject : BaseEntity /// /// Gets or sets the course ident /// + [AuditReference(typeof(Course))] public string CourseId { get; set; } } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Customers/Customer.cs b/src/Core/Grand.Domain/Customers/Customer.cs index 71899c3246..c64c76dea7 100644 --- a/src/Core/Grand.Domain/Customers/Customer.cs +++ b/src/Core/Grand.Domain/Customers/Customer.cs @@ -1,3 +1,6 @@ +using Grand.Domain.Affiliates; +using Grand.Domain.Vendors; +using Grand.Domain.Stores; using Grand.Domain.Audit; using Grand.Domain.Common; using Grand.Domain.Orders; @@ -73,26 +76,31 @@ public Customer() /// /// Gets or sets the affiliate identifier /// + [AuditReference(typeof(Affiliate))] public string AffiliateId { get; set; } /// /// Gets or sets the vendor identifier with which this customer is associated (manager) /// + [AuditReference(typeof(Vendor))] public string VendorId { get; set; } /// /// Gets or sets the store identifier /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// /// Gets or sets the staff store identifier /// + [AuditReference(typeof(Store))] public string StaffStoreId { get; set; } /// /// Gets or sets the owner identifier /// + [AuditReference(typeof(Customer))] public string OwnerId { get; set; } /// @@ -196,6 +204,7 @@ public Customer() /// /// Gets or sets the customer groups /// + [AuditReference(typeof(CustomerGroup))] public virtual ICollection Groups { get => _groups ??= new List(); protected set => _groups = value; @@ -235,6 +244,7 @@ public virtual ICollection
Addresses { /// /// Gets or sets the customer tags /// + [AuditReference(typeof(CustomerTag))] public virtual ICollection CustomerTags { get => _customerTags ??= new List(); protected set => _customerTags = value; diff --git a/src/Core/Grand.Domain/Customers/CustomerAttribute.cs b/src/Core/Grand.Domain/Customers/CustomerAttribute.cs index f288021329..8fa2d87422 100644 --- a/src/Core/Grand.Domain/Customers/CustomerAttribute.cs +++ b/src/Core/Grand.Domain/Customers/CustomerAttribute.cs @@ -1,3 +1,4 @@ +using Grand.Domain.Audit; using Grand.Domain.Catalog; using Grand.Domain.Localization; using Grand.Domain.Stores; @@ -54,5 +55,6 @@ public virtual ICollection CustomerAttributeValues { ///
public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Customers/CustomerAttributeValue.cs b/src/Core/Grand.Domain/Customers/CustomerAttributeValue.cs index fcbcb0f231..3fa19b840d 100644 --- a/src/Core/Grand.Domain/Customers/CustomerAttributeValue.cs +++ b/src/Core/Grand.Domain/Customers/CustomerAttributeValue.cs @@ -1,3 +1,4 @@ +using Grand.Domain.Audit; using Grand.Domain.Localization; namespace Grand.Domain.Customers; @@ -10,6 +11,7 @@ public class CustomerAttributeValue : SubBaseEntity, ITranslationEntity /// /// Gets or sets the customer attribute identifier /// + [AuditReference(typeof(CustomerAttribute))] public string CustomerAttributeId { get; set; } /// diff --git a/src/Core/Grand.Domain/Customers/CustomerGroupProduct.cs b/src/Core/Grand.Domain/Customers/CustomerGroupProduct.cs index ebab17b020..9aae087258 100644 --- a/src/Core/Grand.Domain/Customers/CustomerGroupProduct.cs +++ b/src/Core/Grand.Domain/Customers/CustomerGroupProduct.cs @@ -1,3 +1,6 @@ +using Grand.Domain.Catalog; +using Grand.Domain.Audit; + namespace Grand.Domain.Customers; /// @@ -8,11 +11,13 @@ public class CustomerGroupProduct : BaseEntity /// /// Gets or sets the customer group id /// + [AuditReference(typeof(CustomerGroup))] public string CustomerGroupId { get; set; } /// /// Gets or sets the product Id /// + [AuditReference(typeof(Product))] public string ProductId { get; set; } /// diff --git a/src/Core/Grand.Domain/Customers/CustomerNote.cs b/src/Core/Grand.Domain/Customers/CustomerNote.cs index a37d3cbdd8..7600141c28 100644 --- a/src/Core/Grand.Domain/Customers/CustomerNote.cs +++ b/src/Core/Grand.Domain/Customers/CustomerNote.cs @@ -10,6 +10,7 @@ public class CustomerNote : BaseEntity /// /// Gets or sets the customer identifier /// + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } /// diff --git a/src/Core/Grand.Domain/Customers/CustomerProduct.cs b/src/Core/Grand.Domain/Customers/CustomerProduct.cs index 6fde63bf87..b6e472061d 100644 --- a/src/Core/Grand.Domain/Customers/CustomerProduct.cs +++ b/src/Core/Grand.Domain/Customers/CustomerProduct.cs @@ -1,7 +1,10 @@ -namespace Grand.Domain.Customers; +using Grand.Domain.Audit; + +namespace Grand.Domain.Customers; public class CustomerProduct : BaseEntity { + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } public string ProductId { get; set; } public int DisplayOrder { get; set; } diff --git a/src/Core/Grand.Domain/Customers/CustomerProductPrice.cs b/src/Core/Grand.Domain/Customers/CustomerProductPrice.cs index 751a1b1b0c..eade38d407 100644 --- a/src/Core/Grand.Domain/Customers/CustomerProductPrice.cs +++ b/src/Core/Grand.Domain/Customers/CustomerProductPrice.cs @@ -1,7 +1,10 @@ -namespace Grand.Domain.Customers; +using Grand.Domain.Audit; + +namespace Grand.Domain.Customers; public class CustomerProductPrice : BaseEntity { + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } public string ProductId { get; set; } public double Price { get; set; } diff --git a/src/Core/Grand.Domain/Customers/CustomerTagProduct.cs b/src/Core/Grand.Domain/Customers/CustomerTagProduct.cs index 35b5d2e2b4..cbd17647ae 100644 --- a/src/Core/Grand.Domain/Customers/CustomerTagProduct.cs +++ b/src/Core/Grand.Domain/Customers/CustomerTagProduct.cs @@ -1,3 +1,6 @@ +using Grand.Domain.Catalog; +using Grand.Domain.Audit; + namespace Grand.Domain.Customers; /// @@ -8,11 +11,13 @@ public class CustomerTagProduct : BaseEntity /// /// Gets or sets the customer tag id /// + [AuditReference(typeof(CustomerTag))] public string CustomerTagId { get; set; } /// /// Gets or sets the product Id /// + [AuditReference(typeof(Product))] public string ProductId { get; set; } /// diff --git a/src/Core/Grand.Domain/Customers/ExternalAuthentication.cs b/src/Core/Grand.Domain/Customers/ExternalAuthentication.cs index 82742504fb..924c41ba17 100644 --- a/src/Core/Grand.Domain/Customers/ExternalAuthentication.cs +++ b/src/Core/Grand.Domain/Customers/ExternalAuthentication.cs @@ -1,3 +1,4 @@ +using Grand.Domain.Stores; using Grand.Domain.Audit; namespace Grand.Domain.Customers; @@ -10,6 +11,7 @@ public class ExternalAuthentication : BaseEntity /// /// Gets or sets the customer identifier /// + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } /// @@ -41,6 +43,7 @@ public class ExternalAuthentication : BaseEntity /// /// Gets or sets the store of the linked customer when customers are registered per store; empty otherwise /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// diff --git a/src/Core/Grand.Domain/Customers/ScimGroup.cs b/src/Core/Grand.Domain/Customers/ScimGroup.cs index 527d3e3b80..585c21a5f7 100644 --- a/src/Core/Grand.Domain/Customers/ScimGroup.cs +++ b/src/Core/Grand.Domain/Customers/ScimGroup.cs @@ -1,3 +1,7 @@ +using Grand.Domain.Vendors; +using Grand.Domain.Stores; +using Grand.Domain.Audit; + namespace Grand.Domain.Customers; /// @@ -15,11 +19,14 @@ public class ScimGroup : BaseEntity public List Members { get; set; } = []; /// Customer group the members get; empty while the group is not mapped + [AuditReference(typeof(CustomerGroup))] public string CustomerGroupId { get; set; } /// Staff store of the members, with the StoreManager group + [AuditReference(typeof(Store))] public string StaffStoreId { get; set; } /// Vendor of the members, with the Vendors group + [AuditReference(typeof(Vendor))] public string VendorId { get; set; } } diff --git a/src/Core/Grand.Domain/Customers/ScimUser.cs b/src/Core/Grand.Domain/Customers/ScimUser.cs index 908cb724a2..f3e87e4030 100644 --- a/src/Core/Grand.Domain/Customers/ScimUser.cs +++ b/src/Core/Grand.Domain/Customers/ScimUser.cs @@ -1,3 +1,7 @@ +using Grand.Domain.Vendors; +using Grand.Domain.Stores; +using Grand.Domain.Audit; + namespace Grand.Domain.Customers; /// @@ -13,9 +17,11 @@ public class ScimUser : BaseEntity public List GrantedGroupIds { get; set; } = []; /// Staff store SCIM set on the account; empty when SCIM set none + [AuditReference(typeof(Store))] public string GrantedStaffStoreId { get; set; } /// Vendor SCIM set on the account; empty when SCIM set none + [AuditReference(typeof(Vendor))] public string GrantedVendorId { get; set; } /// The account was deactivated by SCIM, so SCIM may activate it again (never an account disabled by hand) diff --git a/src/Core/Grand.Domain/Directory/Country.cs b/src/Core/Grand.Domain/Directory/Country.cs index 9aa57c3df1..33dd6ba0bc 100644 --- a/src/Core/Grand.Domain/Directory/Country.cs +++ b/src/Core/Grand.Domain/Directory/Country.cs @@ -1,3 +1,4 @@ +using Grand.Domain.Audit; using Grand.Domain.Localization; using Grand.Domain.Stores; @@ -68,6 +69,7 @@ public virtual ICollection StateProvinces { /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); /// diff --git a/src/Core/Grand.Domain/Directory/Currency.cs b/src/Core/Grand.Domain/Directory/Currency.cs index 72fa003d5d..0e047f749e 100644 --- a/src/Core/Grand.Domain/Directory/Currency.cs +++ b/src/Core/Grand.Domain/Directory/Currency.cs @@ -1,3 +1,4 @@ +using Grand.Domain.Audit; using Grand.Domain.Localization; using Grand.Domain.Stores; @@ -63,6 +64,7 @@ public class Currency : BaseEntity, ITranslationEntity, IStoreLinkEntity /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); /// diff --git a/src/Core/Grand.Domain/Discounts/Discount.cs b/src/Core/Grand.Domain/Discounts/Discount.cs index 47bb33fab8..caba772289 100644 --- a/src/Core/Grand.Domain/Discounts/Discount.cs +++ b/src/Core/Grand.Domain/Discounts/Discount.cs @@ -1,4 +1,5 @@ -using Grand.Domain.Stores; +using Grand.Domain.Audit; +using Grand.Domain.Stores; namespace Grand.Domain.Discounts; @@ -115,5 +116,6 @@ public virtual ICollection DiscountRules { /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Discounts/DiscountCoupon.cs b/src/Core/Grand.Domain/Discounts/DiscountCoupon.cs index fa0cdc8c6b..fa69a6acf3 100644 --- a/src/Core/Grand.Domain/Discounts/DiscountCoupon.cs +++ b/src/Core/Grand.Domain/Discounts/DiscountCoupon.cs @@ -13,6 +13,7 @@ public class DiscountCoupon : BaseEntity /// /// Gets or sets the discount id /// + [AuditReference(typeof(Discount))] public string DiscountId { get; set; } /// diff --git a/src/Core/Grand.Domain/Discounts/DiscountUsageHistory.cs b/src/Core/Grand.Domain/Discounts/DiscountUsageHistory.cs index a0d2e166af..40a2c7911a 100644 --- a/src/Core/Grand.Domain/Discounts/DiscountUsageHistory.cs +++ b/src/Core/Grand.Domain/Discounts/DiscountUsageHistory.cs @@ -1,3 +1,7 @@ +using Grand.Domain.Orders; +using Grand.Domain.Customers; +using Grand.Domain.Audit; + namespace Grand.Domain.Discounts; /// @@ -8,6 +12,7 @@ public class DiscountUsageHistory : BaseEntity /// /// Gets or sets the discount identifier /// + [AuditReference(typeof(Discount))] public string DiscountId { get; set; } /// @@ -18,11 +23,13 @@ public class DiscountUsageHistory : BaseEntity /// /// Gets or sets the order identifier /// + [AuditReference(typeof(Order))] public string OrderId { get; set; } /// /// Gets or sets the customer identifier /// + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } /// diff --git a/src/Core/Grand.Domain/Documents/Document.cs b/src/Core/Grand.Domain/Documents/Document.cs index 3580a5cd93..3dab3fb887 100644 --- a/src/Core/Grand.Domain/Documents/Document.cs +++ b/src/Core/Grand.Domain/Documents/Document.cs @@ -1,4 +1,6 @@ -using Grand.Domain.Common; +using Grand.Domain.Customers; +using Grand.Domain.Audit; +using Grand.Domain.Common; using Grand.Domain.Permissions; using Grand.Domain.Stores; @@ -32,6 +34,7 @@ public class Document : BaseEntity, IGroupLinkEntity, IStoreLinkEntity /// /// Gets or sets the parent document identifier /// + [AuditReference(typeof(Document))] public string ParentDocumentId { get; set; } /// @@ -67,6 +70,7 @@ public class Document : BaseEntity, IGroupLinkEntity, IStoreLinkEntity /// /// Gets or sets the customer identifier /// + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } /// @@ -87,6 +91,7 @@ public class Document : BaseEntity, IGroupLinkEntity, IStoreLinkEntity /// /// Gets or sets an document type identifier (DocumentType) /// + [AuditReference(typeof(DocumentType))] public string DocumentTypeId { get; set; } /// @@ -134,6 +139,7 @@ public class Document : BaseEntity, IGroupLinkEntity, IStoreLinkEntity /// public bool LimitedToGroups { get; set; } + [AuditReference(typeof(CustomerGroup))] public IList CustomerGroups { get; set; } = new List(); /// @@ -141,5 +147,6 @@ public class Document : BaseEntity, IGroupLinkEntity, IStoreLinkEntity /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Knowledgebase/KnowledgebaseArticle.cs b/src/Core/Grand.Domain/Knowledgebase/KnowledgebaseArticle.cs index d94c6f79e4..f8d140a273 100644 --- a/src/Core/Grand.Domain/Knowledgebase/KnowledgebaseArticle.cs +++ b/src/Core/Grand.Domain/Knowledgebase/KnowledgebaseArticle.cs @@ -1,4 +1,6 @@ -using Grand.Domain.Localization; +using Grand.Domain.Customers; +using Grand.Domain.Audit; +using Grand.Domain.Localization; using Grand.Domain.Permissions; using Grand.Domain.Seo; using Grand.Domain.Stores; @@ -58,6 +60,7 @@ public class KnowledgebaseArticle : BaseEntity, ITreeNode, ITranslationEntity, I /// public bool LimitedToGroups { get; set; } + [AuditReference(typeof(CustomerGroup))] public IList CustomerGroups { get; set; } = new List(); /// @@ -70,6 +73,7 @@ public class KnowledgebaseArticle : BaseEntity, ITreeNode, ITranslationEntity, I /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); /// @@ -85,5 +89,6 @@ public class KnowledgebaseArticle : BaseEntity, ITreeNode, ITranslationEntity, I /// /// Gets or sets parent category Id /// + [AuditReference(typeof(KnowledgebaseCategory))] public string ParentCategoryId { get; set; } } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Knowledgebase/KnowledgebaseArticleComment.cs b/src/Core/Grand.Domain/Knowledgebase/KnowledgebaseArticleComment.cs index 2ba337c4cc..92352f0d2c 100644 --- a/src/Core/Grand.Domain/Knowledgebase/KnowledgebaseArticleComment.cs +++ b/src/Core/Grand.Domain/Knowledgebase/KnowledgebaseArticleComment.cs @@ -1,10 +1,14 @@ -namespace Grand.Domain.Knowledgebase; +using Grand.Domain.Customers; +using Grand.Domain.Audit; + +namespace Grand.Domain.Knowledgebase; public class KnowledgebaseArticleComment : BaseEntity { /// /// Gets or sets the customer identifier /// + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } /// @@ -20,5 +24,6 @@ public class KnowledgebaseArticleComment : BaseEntity /// /// Gets or sets the blog post identifier /// + [AuditReference(typeof(KnowledgebaseArticle))] public string ArticleId { get; set; } } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Knowledgebase/KnowledgebaseCategory.cs b/src/Core/Grand.Domain/Knowledgebase/KnowledgebaseCategory.cs index 8f1da63319..83773d68e3 100644 --- a/src/Core/Grand.Domain/Knowledgebase/KnowledgebaseCategory.cs +++ b/src/Core/Grand.Domain/Knowledgebase/KnowledgebaseCategory.cs @@ -1,4 +1,6 @@ -using Grand.Domain.Localization; +using Grand.Domain.Customers; +using Grand.Domain.Audit; +using Grand.Domain.Localization; using Grand.Domain.Permissions; using Grand.Domain.Seo; using Grand.Domain.Stores; @@ -43,6 +45,7 @@ public class KnowledgebaseCategory : BaseEntity, ITreeNode, ITranslationEntity, /// public bool LimitedToGroups { get; set; } + [AuditReference(typeof(CustomerGroup))] public IList CustomerGroups { get; set; } = new List(); /// @@ -55,6 +58,7 @@ public class KnowledgebaseCategory : BaseEntity, ITreeNode, ITranslationEntity, /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); /// @@ -70,5 +74,6 @@ public class KnowledgebaseCategory : BaseEntity, ITreeNode, ITranslationEntity, /// /// Gets or sets parent category Id /// + [AuditReference(typeof(KnowledgebaseCategory))] public string ParentCategoryId { get; set; } } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Localization/Language.cs b/src/Core/Grand.Domain/Localization/Language.cs index a9767ca82d..e55af1b0b9 100644 --- a/src/Core/Grand.Domain/Localization/Language.cs +++ b/src/Core/Grand.Domain/Localization/Language.cs @@ -1,3 +1,5 @@ +using Grand.Domain.Directory; +using Grand.Domain.Audit; using Grand.Domain.Stores; namespace Grand.Domain.Localization; @@ -36,6 +38,7 @@ public class Language : BaseEntity, IStoreLinkEntity /// Gets or sets the identifier of the default currency for this language; 0 is set when we use the default currency /// display order /// + [AuditReference(typeof(Currency))] public string DefaultCurrencyId { get; set; } /// @@ -53,5 +56,6 @@ public class Language : BaseEntity, IStoreLinkEntity /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Messages/Campaign.cs b/src/Core/Grand.Domain/Messages/Campaign.cs index d1f59d4df2..64d100d682 100644 --- a/src/Core/Grand.Domain/Messages/Campaign.cs +++ b/src/Core/Grand.Domain/Messages/Campaign.cs @@ -1,4 +1,9 @@ -namespace Grand.Domain.Messages; +using Grand.Domain.Stores; +using Grand.Domain.Localization; +using Grand.Domain.Customers; +using Grand.Domain.Audit; + +namespace Grand.Domain.Messages; /// /// Represents a campaign @@ -27,11 +32,13 @@ public class Campaign : BaseEntity /// /// Gets or sets the store identifier /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// /// Gets or sets the lang identifier /// + [AuditReference(typeof(Language))] public string LanguageId { get; set; } public DateTime? CustomerCreatedDateFrom { get; set; } @@ -48,11 +55,13 @@ public class Campaign : BaseEntity /// /// Gets or sets the used email account identifier /// + [AuditReference(typeof(EmailAccount))] public string EmailAccountId { get; set; } /// /// Gets or sets the customer tags /// + [AuditReference(typeof(CustomerTag))] public virtual ICollection CustomerTags { get => _customerTags ??= new List(); protected set => _customerTags = value; @@ -61,6 +70,7 @@ public virtual ICollection CustomerTags { /// /// Gets or sets the customer groups /// + [AuditReference(typeof(CustomerGroup))] public virtual ICollection CustomerGroups { get => _customerGroups ??= new List(); protected set => _customerGroups = value; diff --git a/src/Core/Grand.Domain/Messages/ContactAttribute.cs b/src/Core/Grand.Domain/Messages/ContactAttribute.cs index 931cf143c7..2c5c247678 100644 --- a/src/Core/Grand.Domain/Messages/ContactAttribute.cs +++ b/src/Core/Grand.Domain/Messages/ContactAttribute.cs @@ -1,3 +1,5 @@ +using Grand.Domain.Customers; +using Grand.Domain.Audit; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Localization; @@ -90,6 +92,7 @@ public virtual ICollection ContactAttributeValues { /// public bool LimitedToGroups { get; set; } + [AuditReference(typeof(CustomerGroup))] public IList CustomerGroups { get; set; } = new List(); /// @@ -97,6 +100,7 @@ public virtual ICollection ContactAttributeValues { /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); diff --git a/src/Core/Grand.Domain/Messages/ContactAttributeValue.cs b/src/Core/Grand.Domain/Messages/ContactAttributeValue.cs index b39fbb49d2..a09c301009 100644 --- a/src/Core/Grand.Domain/Messages/ContactAttributeValue.cs +++ b/src/Core/Grand.Domain/Messages/ContactAttributeValue.cs @@ -1,3 +1,4 @@ +using Grand.Domain.Audit; using Grand.Domain.Localization; namespace Grand.Domain.Messages; @@ -10,6 +11,7 @@ public class ContactAttributeValue : SubBaseEntity, ITranslationEntity /// /// Gets or sets the checkout attribute mapping identifier /// + [AuditReference(typeof(ContactAttribute))] public string ContactAttributeId { get; set; } /// diff --git a/src/Core/Grand.Domain/Messages/ContactUs.cs b/src/Core/Grand.Domain/Messages/ContactUs.cs index 5a8713c31f..2ebf632e00 100644 --- a/src/Core/Grand.Domain/Messages/ContactUs.cs +++ b/src/Core/Grand.Domain/Messages/ContactUs.cs @@ -1,4 +1,7 @@ -using Grand.Domain.Audit; +using Grand.Domain.Vendors; +using Grand.Domain.Stores; +using Grand.Domain.Customers; +using Grand.Domain.Audit; using Grand.Domain.Common; namespace Grand.Domain.Messages; @@ -11,11 +14,13 @@ public class ContactUs : BaseEntity /// /// Gets or sets the customer identifier /// + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } /// /// Gets or sets the store identifier /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// @@ -47,11 +52,13 @@ public class ContactUs : BaseEntity /// /// Gets or sets the email account identifier /// + [AuditReference(typeof(EmailAccount))] public string EmailAccountId { get; set; } /// /// Gets or sets the vendor identifier /// + [AuditReference(typeof(Vendor))] public string VendorId { get; set; } /// diff --git a/src/Core/Grand.Domain/Messages/EmailAccount.cs b/src/Core/Grand.Domain/Messages/EmailAccount.cs index ef756fa671..a6ff523982 100644 --- a/src/Core/Grand.Domain/Messages/EmailAccount.cs +++ b/src/Core/Grand.Domain/Messages/EmailAccount.cs @@ -1,4 +1,5 @@ -using Grand.Domain.Audit; +using Grand.Domain.Stores; +using Grand.Domain.Audit; namespace Grand.Domain.Messages; @@ -53,5 +54,6 @@ public class EmailAccount : BaseEntity /// Gets or sets the store identifier this email account belongs to. /// An empty string means this is a global/shared email account. /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Messages/MessageTemplate.cs b/src/Core/Grand.Domain/Messages/MessageTemplate.cs index 494783c91e..0864b110f2 100644 --- a/src/Core/Grand.Domain/Messages/MessageTemplate.cs +++ b/src/Core/Grand.Domain/Messages/MessageTemplate.cs @@ -1,4 +1,5 @@ -using Grand.Domain.Localization; +using Grand.Domain.Audit; +using Grand.Domain.Localization; using Grand.Domain.Stores; namespace Grand.Domain.Messages; @@ -51,6 +52,7 @@ public class MessageTemplate : BaseEntity, ITranslationEntity, IStoreLinkEntity /// /// Gets or sets the used email account identifier /// + [AuditReference(typeof(EmailAccount))] public string EmailAccountId { get; set; } /// @@ -58,6 +60,7 @@ public class MessageTemplate : BaseEntity, ITranslationEntity, IStoreLinkEntity /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); diff --git a/src/Core/Grand.Domain/Messages/NewsLetterSubscription.cs b/src/Core/Grand.Domain/Messages/NewsLetterSubscription.cs index 395b054ffc..73b56df452 100644 --- a/src/Core/Grand.Domain/Messages/NewsLetterSubscription.cs +++ b/src/Core/Grand.Domain/Messages/NewsLetterSubscription.cs @@ -1,4 +1,6 @@ -using Grand.Domain.Audit; +using Grand.Domain.Stores; +using Grand.Domain.Customers; +using Grand.Domain.Audit; using Grand.Domain.History; namespace Grand.Domain.Messages; @@ -18,6 +20,7 @@ public class NewsLetterSubscription : BaseEntity, IHistory /// /// Gets or sets the Customer identifier /// + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } /// @@ -34,11 +37,13 @@ public class NewsLetterSubscription : BaseEntity, IHistory /// /// Gets or sets the store identifier in which a customer has subscribed to newsletter /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// /// Gets or sets the categories /// + [AuditReference(typeof(NewsletterCategory))] public virtual ICollection Categories { get => _categories ??= new List(); protected set => _categories = value; diff --git a/src/Core/Grand.Domain/Messages/NewsletterCategory.cs b/src/Core/Grand.Domain/Messages/NewsletterCategory.cs index 8278bf4573..ba1be5db6e 100644 --- a/src/Core/Grand.Domain/Messages/NewsletterCategory.cs +++ b/src/Core/Grand.Domain/Messages/NewsletterCategory.cs @@ -1,4 +1,5 @@ -using Grand.Domain.Localization; +using Grand.Domain.Audit; +using Grand.Domain.Localization; using Grand.Domain.Stores; namespace Grand.Domain.Messages; @@ -27,6 +28,7 @@ public class NewsletterCategory : BaseEntity, ITranslationEntity, IStoreLinkEnti /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); public IList Locales { get; set; } = new List(); diff --git a/src/Core/Grand.Domain/News/NewsComment.cs b/src/Core/Grand.Domain/News/NewsComment.cs index 282b457732..3559c2480c 100644 --- a/src/Core/Grand.Domain/News/NewsComment.cs +++ b/src/Core/Grand.Domain/News/NewsComment.cs @@ -1,3 +1,7 @@ +using Grand.Domain.Stores; +using Grand.Domain.Customers; +using Grand.Domain.Audit; + namespace Grand.Domain.News; /// @@ -18,16 +22,19 @@ public class NewsComment : SubBaseEntity /// /// Gets or sets the news item identifier /// + [AuditReference(typeof(NewsItem))] public string NewsItemId { get; set; } /// /// Gets or sets the customer identifier /// + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } /// /// Gets or sets the store identifier /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// diff --git a/src/Core/Grand.Domain/News/NewsItem.cs b/src/Core/Grand.Domain/News/NewsItem.cs index c712b5d29a..d7b0a96a19 100644 --- a/src/Core/Grand.Domain/News/NewsItem.cs +++ b/src/Core/Grand.Domain/News/NewsItem.cs @@ -1,3 +1,4 @@ +using Grand.Domain.Customers; using Grand.Domain.Audit; using Grand.Domain.Localization; using Grand.Domain.Permissions; @@ -90,6 +91,7 @@ public virtual ICollection NewsComments { /// public bool LimitedToGroups { get; set; } + [AuditReference(typeof(CustomerGroup))] public IList CustomerGroups { get; set; } = new List(); /// @@ -102,6 +104,7 @@ public virtual ICollection NewsComments { /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); /// diff --git a/src/Core/Grand.Domain/Orders/CheckoutAttribute.cs b/src/Core/Grand.Domain/Orders/CheckoutAttribute.cs index a601eca13c..6e38cbeb66 100644 --- a/src/Core/Grand.Domain/Orders/CheckoutAttribute.cs +++ b/src/Core/Grand.Domain/Orders/CheckoutAttribute.cs @@ -1,3 +1,6 @@ +using Grand.Domain.Tax; +using Grand.Domain.Customers; +using Grand.Domain.Audit; using Grand.Domain.Catalog; using Grand.Domain.Common; using Grand.Domain.Localization; @@ -41,6 +44,7 @@ public class CheckoutAttribute : BaseEntity, ITranslationEntity, IStoreLinkEntit /// /// Gets or sets the tax category identifier /// + [AuditReference(typeof(TaxCategory))] public string TaxCategoryId { get; set; } /// @@ -98,6 +102,7 @@ public virtual ICollection CheckoutAttributeValues { /// public bool LimitedToGroups { get; set; } + [AuditReference(typeof(CustomerGroup))] public IList CustomerGroups { get; set; } = new List(); /// @@ -105,6 +110,7 @@ public virtual ICollection CheckoutAttributeValues { /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); diff --git a/src/Core/Grand.Domain/Orders/CheckoutAttributeValue.cs b/src/Core/Grand.Domain/Orders/CheckoutAttributeValue.cs index eac7bf62fd..c28903c23f 100644 --- a/src/Core/Grand.Domain/Orders/CheckoutAttributeValue.cs +++ b/src/Core/Grand.Domain/Orders/CheckoutAttributeValue.cs @@ -1,3 +1,4 @@ +using Grand.Domain.Audit; using Grand.Domain.Localization; namespace Grand.Domain.Orders; @@ -10,6 +11,7 @@ public class CheckoutAttributeValue : SubBaseEntity, ITranslationEntity /// /// Gets or sets the checkout attribute mapping identifier /// + [AuditReference(typeof(CheckoutAttribute))] public string CheckoutAttributeId { get; set; } /// diff --git a/src/Core/Grand.Domain/Orders/GiftVoucher.cs b/src/Core/Grand.Domain/Orders/GiftVoucher.cs index 5fde714341..cdee9f1ea2 100644 --- a/src/Core/Grand.Domain/Orders/GiftVoucher.cs +++ b/src/Core/Grand.Domain/Orders/GiftVoucher.cs @@ -1,3 +1,4 @@ +using Grand.Domain.Stores; using Grand.Domain.Audit; using Grand.Domain.Catalog; @@ -69,6 +70,7 @@ public class GiftVoucher : BaseEntity /// /// Gets or sets the store identifier /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// diff --git a/src/Core/Grand.Domain/Orders/GiftVoucherUsageHistory.cs b/src/Core/Grand.Domain/Orders/GiftVoucherUsageHistory.cs index be296f6ca6..e53a3b2de2 100644 --- a/src/Core/Grand.Domain/Orders/GiftVoucherUsageHistory.cs +++ b/src/Core/Grand.Domain/Orders/GiftVoucherUsageHistory.cs @@ -1,3 +1,5 @@ +using Grand.Domain.Audit; + namespace Grand.Domain.Orders; /// @@ -8,11 +10,13 @@ public class GiftVoucherUsageHistory : SubBaseEntity /// /// Gets or sets the gift voucher identifier /// + [AuditReference(typeof(GiftVoucher))] public string GiftVoucherId { get; set; } /// /// Gets or sets the order identifier /// + [AuditReference(typeof(Order))] public string UsedWithOrderId { get; set; } /// diff --git a/src/Core/Grand.Domain/Orders/LoyaltyPointsHistory.cs b/src/Core/Grand.Domain/Orders/LoyaltyPointsHistory.cs index 9bb0e068b7..de39a032b7 100644 --- a/src/Core/Grand.Domain/Orders/LoyaltyPointsHistory.cs +++ b/src/Core/Grand.Domain/Orders/LoyaltyPointsHistory.cs @@ -1,3 +1,7 @@ +using Grand.Domain.Stores; +using Grand.Domain.Customers; +using Grand.Domain.Audit; + namespace Grand.Domain.Orders; /// @@ -8,11 +12,13 @@ public class LoyaltyPointsHistory : BaseEntity /// /// Gets or sets the customer identifier /// + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } /// /// Gets or sets the store identifier in which these loyalty points were awarded or redeemed /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// @@ -35,5 +41,6 @@ public class LoyaltyPointsHistory : BaseEntity /// public string Message { get; set; } + [AuditReference(typeof(Order))] public string UsedWithOrderId { get; set; } } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Orders/MerchandiseReturn.cs b/src/Core/Grand.Domain/Orders/MerchandiseReturn.cs index 36142f8778..534747bc22 100644 --- a/src/Core/Grand.Domain/Orders/MerchandiseReturn.cs +++ b/src/Core/Grand.Domain/Orders/MerchandiseReturn.cs @@ -1,3 +1,7 @@ +using Grand.Domain.Vendors; +using Grand.Domain.Stores; +using Grand.Domain.Customers; +using Grand.Domain.Audit; using Grand.Domain.Common; namespace Grand.Domain.Orders; @@ -17,16 +21,19 @@ public class MerchandiseReturn : BaseEntity /// /// Gets or sets the store identifier /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// /// Gets or sets the order item identifier /// + [AuditReference(typeof(Order))] public string OrderId { get; set; } /// /// Gets or sets the owner item identifier /// + [AuditReference(typeof(Customer))] public string OwnerId { get; set; } /// @@ -42,11 +49,13 @@ public class MerchandiseReturn : BaseEntity /// /// Gets or sets the customer identifier /// + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } /// /// Gets or sets the vendor item identifier /// + [AuditReference(typeof(Vendor))] public string VendorId { get; set; } /// diff --git a/src/Core/Grand.Domain/Orders/MerchandiseReturnAction.cs b/src/Core/Grand.Domain/Orders/MerchandiseReturnAction.cs index 935b34e121..f65f4d65d9 100644 --- a/src/Core/Grand.Domain/Orders/MerchandiseReturnAction.cs +++ b/src/Core/Grand.Domain/Orders/MerchandiseReturnAction.cs @@ -1,4 +1,5 @@ -using Grand.Domain.Localization; +using Grand.Domain.Audit; +using Grand.Domain.Localization; using Grand.Domain.Stores; namespace Grand.Domain.Orders; @@ -31,5 +32,6 @@ public class MerchandiseReturnAction : BaseEntity, ITranslationEntity, IStoreLin /// /// Gets or sets the stores /// + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Orders/MerchandiseReturnReason.cs b/src/Core/Grand.Domain/Orders/MerchandiseReturnReason.cs index 765c9a081c..5d0af06b99 100644 --- a/src/Core/Grand.Domain/Orders/MerchandiseReturnReason.cs +++ b/src/Core/Grand.Domain/Orders/MerchandiseReturnReason.cs @@ -1,4 +1,5 @@ -using Grand.Domain.Localization; +using Grand.Domain.Audit; +using Grand.Domain.Localization; using Grand.Domain.Stores; namespace Grand.Domain.Orders; @@ -31,5 +32,6 @@ public class MerchandiseReturnReason : BaseEntity, ITranslationEntity, IStoreLin /// /// Gets or sets the stores /// + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Orders/Order.cs b/src/Core/Grand.Domain/Orders/Order.cs index 63dca98211..f4116b0eda 100644 --- a/src/Core/Grand.Domain/Orders/Order.cs +++ b/src/Core/Grand.Domain/Orders/Order.cs @@ -1,3 +1,7 @@ +using Grand.Domain.Localization; +using Grand.Domain.Affiliates; +using Grand.Domain.Stores; +using Grand.Domain.Customers; using Grand.Domain.Audit; using Grand.Domain.Catalog; using Grand.Domain.Common; @@ -37,16 +41,19 @@ public class Order : BaseEntity /// /// Gets or sets the store identifier /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// /// Gets or sets the customer identifier /// + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } /// /// Gets or sets the owner identifier /// + [AuditReference(typeof(Customer))] public string OwnerId { get; set; } /// @@ -62,6 +69,7 @@ public class Order : BaseEntity /// /// Gets or sets an order status identifier /// + [AuditReference(typeof(OrderStatus), Key = nameof(OrderStatus.StatusId))] public int OrderStatusId { get; set; } /// @@ -228,11 +236,13 @@ public class Order : BaseEntity /// /// Gets or sets the customer language identifier /// + [AuditReference(typeof(Language))] public string CustomerLanguageId { get; set; } /// /// Gets or sets the affiliate identifier /// + [AuditReference(typeof(Affiliate))] public string AffiliateId { get; set; } /// diff --git a/src/Core/Grand.Domain/Orders/OrderApproval.cs b/src/Core/Grand.Domain/Orders/OrderApproval.cs index 3d6e050ffc..a81747092d 100644 --- a/src/Core/Grand.Domain/Orders/OrderApproval.cs +++ b/src/Core/Grand.Domain/Orders/OrderApproval.cs @@ -1,3 +1,7 @@ +using Grand.Domain.Stores; +using Grand.Domain.Customers; +using Grand.Domain.Audit; + namespace Grand.Domain.Orders; /// @@ -8,16 +12,19 @@ public class OrderApproval : BaseEntity /// /// Gets or sets the store identifier the order belongs to /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// /// Gets or sets the identifier of the company owner; every read is scoped by it /// + [AuditReference(typeof(Customer))] public string CompanyOwnerId { get; set; } /// /// Gets or sets the buyer identifier /// + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } /// @@ -53,6 +60,7 @@ public class OrderApproval : BaseEntity /// /// Gets or sets the identifier of the customer who decided /// + [AuditReference(typeof(Customer))] public string DecidedByCustomerId { get; set; } /// @@ -63,5 +71,6 @@ public class OrderApproval : BaseEntity /// /// Gets or sets the identifier of the order awaiting the decision; set at creation /// + [AuditReference(typeof(Order))] public string OrderId { get; set; } } diff --git a/src/Core/Grand.Domain/Orders/OrderApprovalItem.cs b/src/Core/Grand.Domain/Orders/OrderApprovalItem.cs index 6fd400c6c3..ebfe3504e8 100644 --- a/src/Core/Grand.Domain/Orders/OrderApprovalItem.cs +++ b/src/Core/Grand.Domain/Orders/OrderApprovalItem.cs @@ -1,3 +1,6 @@ +using Grand.Domain.Catalog; +using Grand.Domain.Audit; + namespace Grand.Domain.Orders; /// @@ -8,6 +11,7 @@ public class OrderApprovalItem : SubBaseEntity /// /// Gets or sets the product identifier /// + [AuditReference(typeof(Product))] public string ProductId { get; set; } /// diff --git a/src/Core/Grand.Domain/Orders/OrderItem.cs b/src/Core/Grand.Domain/Orders/OrderItem.cs index d8059b479a..3153fadaa8 100644 --- a/src/Core/Grand.Domain/Orders/OrderItem.cs +++ b/src/Core/Grand.Domain/Orders/OrderItem.cs @@ -1,3 +1,7 @@ +using Grand.Domain.Shipping; +using Grand.Domain.Vendors; +using Grand.Domain.Catalog; +using Grand.Domain.Audit; using Grand.Domain.Common; namespace Grand.Domain.Orders; @@ -15,6 +19,7 @@ public class OrderItem : SubBaseEntity /// /// Gets or sets the product identifier /// + [AuditReference(typeof(Product))] public string ProductId { get; set; } /// @@ -25,11 +30,13 @@ public class OrderItem : SubBaseEntity /// /// Gets or sets the vendor identifier /// + [AuditReference(typeof(Vendor))] public string VendorId { get; set; } /// /// Gets or sets the warehouse identifier /// + [AuditReference(typeof(Warehouse))] public string WarehouseId { get; set; } /// diff --git a/src/Core/Grand.Domain/Orders/OrderNote.cs b/src/Core/Grand.Domain/Orders/OrderNote.cs index fa84a3b4f4..5cc69ebac1 100644 --- a/src/Core/Grand.Domain/Orders/OrderNote.cs +++ b/src/Core/Grand.Domain/Orders/OrderNote.cs @@ -1,3 +1,5 @@ +using Grand.Domain.Audit; + namespace Grand.Domain.Orders; /// @@ -8,6 +10,7 @@ public class OrderNote : BaseEntity /// /// Gets or sets the order identifier /// + [AuditReference(typeof(Order))] public string OrderId { get; set; } /// diff --git a/src/Core/Grand.Domain/Pages/Page.cs b/src/Core/Grand.Domain/Pages/Page.cs index 7b90cff1ca..e27f6c8d4d 100644 --- a/src/Core/Grand.Domain/Pages/Page.cs +++ b/src/Core/Grand.Domain/Pages/Page.cs @@ -1,3 +1,4 @@ +using Grand.Domain.Customers; using Grand.Domain.Audit; using Grand.Domain.Localization; using Grand.Domain.Permissions; @@ -75,6 +76,7 @@ public class Page : BaseEntity, ITranslationEntity, ISlugEntity, IStoreLinkEntit /// /// Gets or sets a value of used page layout identifier /// + [AuditReference(typeof(PageLayout))] public string PageLayoutId { get; set; } /// @@ -112,6 +114,7 @@ public class Page : BaseEntity, ITranslationEntity, ISlugEntity, IStoreLinkEntit /// public bool LimitedToGroups { get; set; } + [AuditReference(typeof(CustomerGroup))] public IList CustomerGroups { get; set; } = new List(); @@ -125,6 +128,7 @@ public class Page : BaseEntity, ITranslationEntity, ISlugEntity, IStoreLinkEntit /// public bool LimitedToStores { get; set; } + [AuditReference(typeof(Store))] public IList Stores { get; set; } = new List(); /// diff --git a/src/Core/Grand.Domain/Payments/PaymentTransaction.cs b/src/Core/Grand.Domain/Payments/PaymentTransaction.cs index f3422a48eb..d9ba05fa5f 100644 --- a/src/Core/Grand.Domain/Payments/PaymentTransaction.cs +++ b/src/Core/Grand.Domain/Payments/PaymentTransaction.cs @@ -1,4 +1,8 @@ -namespace Grand.Domain.Payments; +using Grand.Domain.Stores; +using Grand.Domain.Customers; +using Grand.Domain.Audit; + +namespace Grand.Domain.Payments; public class PaymentTransaction : BaseEntity { @@ -15,6 +19,7 @@ public class PaymentTransaction : BaseEntity /// /// Gets or sets store ident /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// @@ -30,6 +35,7 @@ public class PaymentTransaction : BaseEntity /// /// Gets or sets customer ident /// + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } /// diff --git a/src/Core/Grand.Domain/Permissions/Permission.cs b/src/Core/Grand.Domain/Permissions/Permission.cs index 30781a7924..0bb413c91c 100644 --- a/src/Core/Grand.Domain/Permissions/Permission.cs +++ b/src/Core/Grand.Domain/Permissions/Permission.cs @@ -1,4 +1,7 @@ -namespace Grand.Domain.Permissions; +using Grand.Domain.Customers; +using Grand.Domain.Audit; + +namespace Grand.Domain.Permissions; /// /// Represents a permission @@ -31,6 +34,7 @@ public class Permission : BaseEntity /// /// Gets or sets customer groups /// + [AuditReference(typeof(CustomerGroup))] public virtual ICollection CustomerGroups { get => _customerGroups ??= new List(); protected set => _customerGroups = value; diff --git a/src/Core/Grand.Domain/Permissions/PermissionAction.cs b/src/Core/Grand.Domain/Permissions/PermissionAction.cs index fff65a6ef2..db34dc9ab6 100644 --- a/src/Core/Grand.Domain/Permissions/PermissionAction.cs +++ b/src/Core/Grand.Domain/Permissions/PermissionAction.cs @@ -1,4 +1,5 @@ -using Grand.Domain.Audit; +using Grand.Domain.Customers; +using Grand.Domain.Audit; namespace Grand.Domain.Permissions; @@ -16,6 +17,7 @@ public class PermissionAction : BaseEntity /// /// Gets or sets the customer group ident /// + [AuditReference(typeof(CustomerGroup))] public string CustomerGroupId { get; set; } /// diff --git a/src/Core/Grand.Domain/Seo/EntityUrl.cs b/src/Core/Grand.Domain/Seo/EntityUrl.cs index 475e164827..4fd2ab2e7d 100644 --- a/src/Core/Grand.Domain/Seo/EntityUrl.cs +++ b/src/Core/Grand.Domain/Seo/EntityUrl.cs @@ -1,3 +1,6 @@ +using Grand.Domain.Localization; +using Grand.Domain.Audit; + namespace Grand.Domain.Seo; /// @@ -28,6 +31,7 @@ public class EntityUrl : BaseEntity /// /// Gets or sets the language identifier /// + [AuditReference(typeof(Language))] public string LanguageId { get; set; } /// diff --git a/src/Core/Grand.Domain/Shipping/DeliveryDate.cs b/src/Core/Grand.Domain/Shipping/DeliveryDate.cs index 70a151649b..58ad776264 100644 --- a/src/Core/Grand.Domain/Shipping/DeliveryDate.cs +++ b/src/Core/Grand.Domain/Shipping/DeliveryDate.cs @@ -1,3 +1,5 @@ +using Grand.Domain.Stores; +using Grand.Domain.Audit; using Grand.Domain.Localization; namespace Grand.Domain.Shipping; @@ -30,5 +32,6 @@ public class DeliveryDate : BaseEntity, ITranslationEntity /// /// Gets or sets the store identifier. Empty means available to all stores. /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Shipping/PickupPoints.cs b/src/Core/Grand.Domain/Shipping/PickupPoints.cs index 30821f0400..fb8758705c 100644 --- a/src/Core/Grand.Domain/Shipping/PickupPoints.cs +++ b/src/Core/Grand.Domain/Shipping/PickupPoints.cs @@ -1,3 +1,5 @@ +using Grand.Domain.Stores; +using Grand.Domain.Audit; using Grand.Domain.Common; namespace Grand.Domain.Shipping; @@ -30,11 +32,13 @@ public class PickupPoint : BaseEntity /// /// Gets or sets the warehouse identifier /// + [AuditReference(typeof(Warehouse))] public string WarehouseId { get; set; } /// /// Gets or sets a store identifier /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// diff --git a/src/Core/Grand.Domain/Shipping/Shipment.cs b/src/Core/Grand.Domain/Shipping/Shipment.cs index 9740923277..5eb4146091 100644 --- a/src/Core/Grand.Domain/Shipping/Shipment.cs +++ b/src/Core/Grand.Domain/Shipping/Shipment.cs @@ -1,3 +1,8 @@ +using Grand.Domain.Vendors; +using Grand.Domain.Stores; +using Grand.Domain.Orders; +using Grand.Domain.Audit; + namespace Grand.Domain.Shipping; /// @@ -10,16 +15,19 @@ public class Shipment : BaseEntity /// /// Gets or sets the order identifier /// + [AuditReference(typeof(Order))] public string OrderId { get; set; } /// /// Gets or sets the store identifier /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// /// Gets or sets the vendor identifier /// + [AuditReference(typeof(Vendor))] public string VendorId { get; set; } /// diff --git a/src/Core/Grand.Domain/Shipping/ShipmentItem.cs b/src/Core/Grand.Domain/Shipping/ShipmentItem.cs index 31db6f8d20..245bfcf2b0 100644 --- a/src/Core/Grand.Domain/Shipping/ShipmentItem.cs +++ b/src/Core/Grand.Domain/Shipping/ShipmentItem.cs @@ -1,3 +1,5 @@ +using Grand.Domain.Catalog; +using Grand.Domain.Audit; using Grand.Domain.Common; namespace Grand.Domain.Shipping; @@ -15,6 +17,7 @@ public class ShipmentItem : SubBaseEntity /// /// Gets or sets the product /// + [AuditReference(typeof(Product))] public string ProductId { get; set; } /// @@ -30,5 +33,6 @@ public class ShipmentItem : SubBaseEntity /// /// Gets or sets the warehouse identifier /// + [AuditReference(typeof(Warehouse))] public string WarehouseId { get; set; } } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Shipping/ShippingMethod.cs b/src/Core/Grand.Domain/Shipping/ShippingMethod.cs index 060c45f004..a2446dc194 100644 --- a/src/Core/Grand.Domain/Shipping/ShippingMethod.cs +++ b/src/Core/Grand.Domain/Shipping/ShippingMethod.cs @@ -1,3 +1,5 @@ +using Grand.Domain.Stores; +using Grand.Domain.Audit; using Grand.Domain.Directory; using Grand.Domain.Localization; @@ -50,5 +52,6 @@ public virtual ICollection RestrictedGroups { /// /// Gets or sets the store identifier. Empty means available to all stores. /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Shipping/Warehouse.cs b/src/Core/Grand.Domain/Shipping/Warehouse.cs index 0422da638d..f9e253a7d9 100644 --- a/src/Core/Grand.Domain/Shipping/Warehouse.cs +++ b/src/Core/Grand.Domain/Shipping/Warehouse.cs @@ -1,3 +1,5 @@ +using Grand.Domain.Stores; +using Grand.Domain.Audit; using Grand.Domain.Common; namespace Grand.Domain.Shipping; @@ -45,5 +47,6 @@ public class Warehouse : BaseEntity /// /// Gets or sets the store identifier. Empty means available to all stores. /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Stores/Store.cs b/src/Core/Grand.Domain/Stores/Store.cs index 95b18b0031..43389189e1 100644 --- a/src/Core/Grand.Domain/Stores/Store.cs +++ b/src/Core/Grand.Domain/Stores/Store.cs @@ -1,3 +1,6 @@ +using Grand.Domain.Shipping; +using Grand.Domain.Directory; +using Grand.Domain.Audit; using Grand.Domain.Localization; namespace Grand.Domain.Stores; @@ -31,21 +34,25 @@ public class Store : BaseEntity, ITranslationEntity /// Gets or sets the identifier of the default language for this store; "" is set when we use the default language /// display order /// + [AuditReference(typeof(Language))] public string DefaultLanguageId { get; set; } /// /// Gets or sets the identifier of the default waregouse for this store /// + [AuditReference(typeof(Warehouse))] public string DefaultWarehouseId { get; set; } /// /// Gets or sets the identifier of the default country for this store /// + [AuditReference(typeof(Country))] public string DefaultCountryId { get; set; } /// /// Gets or sets the identifier of the default currency for this store /// + [AuditReference(typeof(Currency))] public string DefaultCurrencyId { get; set; } /// diff --git a/src/Core/Grand.Domain/Tasks/ScheduleTask.cs b/src/Core/Grand.Domain/Tasks/ScheduleTask.cs index 6d773f6f6a..f4355bf3db 100644 --- a/src/Core/Grand.Domain/Tasks/ScheduleTask.cs +++ b/src/Core/Grand.Domain/Tasks/ScheduleTask.cs @@ -1,4 +1,7 @@ -namespace Grand.Domain.Tasks; +using Grand.Domain.Stores; +using Grand.Domain.Audit; + +namespace Grand.Domain.Tasks; public class ScheduleTask : BaseEntity { @@ -12,5 +15,6 @@ public class ScheduleTask : BaseEntity public DateTime? LeasedUntilUtc { get; set; } public string LeasedByInstance { get; set; } public int TimeInterval { get; set; } + [AuditReference(typeof(Store))] public string StoreId { get; set; } } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Tax/TaxCategory.cs b/src/Core/Grand.Domain/Tax/TaxCategory.cs index f6236219ca..33425c170c 100644 --- a/src/Core/Grand.Domain/Tax/TaxCategory.cs +++ b/src/Core/Grand.Domain/Tax/TaxCategory.cs @@ -1,3 +1,6 @@ +using Grand.Domain.Stores; +using Grand.Domain.Audit; + namespace Grand.Domain.Tax; /// @@ -18,5 +21,6 @@ public class TaxCategory : BaseEntity /// /// Gets or sets the store identifier (empty = available to all stores) /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } } \ No newline at end of file diff --git a/src/Core/Grand.Domain/Vendors/Vendor.cs b/src/Core/Grand.Domain/Vendors/Vendor.cs index 43f1765fb5..ce8a6f7c0e 100644 --- a/src/Core/Grand.Domain/Vendors/Vendor.cs +++ b/src/Core/Grand.Domain/Vendors/Vendor.cs @@ -1,3 +1,6 @@ +using Grand.Domain.Stores; +using Grand.Domain.Discounts; +using Grand.Domain.Audit; using Grand.Domain.Common; using Grand.Domain.Localization; using Grand.Domain.Seo; @@ -41,6 +44,7 @@ public Vendor() /// /// Gets or sets the store identifier /// + [AuditReference(typeof(Store))] public string StoreId { get; set; } /// @@ -145,6 +149,7 @@ public virtual ICollection VendorNotes { /// /// Gets or sets the collection of applied discounts /// + [AuditReference(typeof(Discount))] public virtual ICollection AppliedDiscounts { get => _appliedDiscounts ??= new List(); protected set => _appliedDiscounts = value; diff --git a/src/Core/Grand.Domain/Vendors/VendorReview.cs b/src/Core/Grand.Domain/Vendors/VendorReview.cs index e96cc838fc..351dbb5cb1 100644 --- a/src/Core/Grand.Domain/Vendors/VendorReview.cs +++ b/src/Core/Grand.Domain/Vendors/VendorReview.cs @@ -1,4 +1,5 @@ -using Grand.Domain.Audit; +using Grand.Domain.Customers; +using Grand.Domain.Audit; namespace Grand.Domain.Vendors; @@ -12,11 +13,13 @@ public class VendorReview : BaseEntity /// /// Gets or sets the customer identifier /// + [AuditReference(typeof(Customer))] public string CustomerId { get; set; } /// /// Gets or sets the vendor identifier /// + [AuditReference(typeof(Vendor))] public string VendorId { get; set; } /// diff --git a/src/Core/Grand.Infrastructure/Audit/AuditDiff.cs b/src/Core/Grand.Infrastructure/Audit/AuditDiff.cs index e8d1b1a709..dd87f6a21d 100644 --- a/src/Core/Grand.Infrastructure/Audit/AuditDiff.cs +++ b/src/Core/Grand.Infrastructure/Audit/AuditDiff.cs @@ -64,7 +64,10 @@ private static bool IsUnder(string path, string parent) || path.StartsWith(parent + "[", StringComparison.Ordinal); } - private static AuditChange Change(string path, string oldValue, string newValue) + /// + /// A change of one path, its values cut to and marked when they were + /// + public static AuditChange Change(string path, string oldValue, string newValue) { var change = new AuditChange { Path = path, diff --git a/src/Core/Grand.Infrastructure/Audit/AuditReferences.cs b/src/Core/Grand.Infrastructure/Audit/AuditReferences.cs new file mode 100644 index 0000000000..b0ef9b6054 --- /dev/null +++ b/src/Core/Grand.Infrastructure/Audit/AuditReferences.cs @@ -0,0 +1,154 @@ +using Grand.Data.Audit; +using Grand.Domain.Audit; +using System.Collections.Concurrent; +using System.Reflection; +using System.Text.RegularExpressions; +using System.Text.Json; + +namespace Grand.Infrastructure.Audit; + +/// +/// Puts names next to the ids of [AuditReference] properties and the numbers of enums when a change is recorded, +/// so the entry stays readable after the referenced record is gone +/// +public partial class AuditReferences(IAuditRecordReader reader) +{ + private static readonly ConcurrentDictionary NameProperties = new(); + private static readonly ConcurrentDictionary<(Type, string), PropertyInfo> Properties = new(); + + private readonly Dictionary<(Type Target, string List, string Owner, string Id), string> _names = new(); + + [GeneratedRegex(@"\[[^\]]*\]")] + private static partial Regex ElementKeys(); + + /// + /// The property that names a record of a type: the one marked [AuditName], else Name + /// + public static string NameProperty(Type type) + { + return NameProperties.GetOrAdd(type, t => + t.GetProperties().FirstOrDefault(p => p.IsDefined(typeof(AuditNameAttribute)))?.Name ?? "Name"); + } + + public async Task Describe(Type entityType, IEnumerable changes, IDictionary before, + IDictionary after) + { + //a cut value is no longer a whole id list; it stays as stored + foreach (var change in changes.Where(c => !c.Masked && !c.Truncated)) + { + var property = PropertyAt(entityType, change.Path); + if (property == null) + continue; + + //a name that cannot be found leaves the ids, never the change out + try + { + var described = AuditDiff.Change(change.Path, + await Describe(property, change.Path, change.OldValue, before), + await Describe(property, change.Path, change.NewValue, after)); + change.OldValue = described.OldValue; + change.NewValue = described.NewValue; + change.Truncated = described.Truncated; + } + catch (Exception) + { + //keep the stored values + } + } + } + + //"ProductSpecificationAttributes[id=1].SpecificationAttributeOptionId" -> the property on the element type + private static PropertyInfo PropertyAt(Type type, string path) + { + //cached by the path without its element keys, so the cache grows with the model, not with the data + return Properties.GetOrAdd((type, ElementKeys().Replace(path, "")), key => FindProperty(key.Item1, key.Item2)); + } + + private static PropertyInfo FindProperty(Type type, string path) + { + PropertyInfo property = null; + foreach (var segment in path.Split('.')) + { + property = type?.GetProperty(segment.Split('[')[0]); + if (property == null) + return null; + + type = ElementType(property.PropertyType) ?? property.PropertyType; + } + + return property; + } + + private static Type ElementType(Type type) + { + return type == typeof(string) + ? null + : type.GetInterfaces().Append(type) + .FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + ?.GetGenericArguments()[0]; + } + + private async Task Describe(PropertyInfo property, string path, string value, + IDictionary snapshot) + { + if (string.IsNullOrEmpty(value)) + return value; + + var reference = property.GetCustomAttribute(); + var enumType = reference?.Target is { IsEnum: true } target + ? target + : Nullable.GetUnderlyingType(property.PropertyType) ?? property.PropertyType; + if (enumType.IsEnum) + return long.TryParse(value, out var number) && Enum.GetName(enumType, Enum.ToObject(enumType, number)) is { } name + ? $"{name} ({value})" + : value; + + if (reference == null) + return value; + + var owner = reference.Owner == null ? null : SiblingValue(snapshot, path, reference.Owner); + if (ElementType(property.PropertyType) == null) + return await Label(reference, owner, value); + + //a list of ids is stored as one JSON array value + var described = new List(); + foreach (var id in JsonSerializer.Deserialize(value) ?? []) + described.Add(await Label(reference, owner, id)); + return string.Join(", ", described); + } + + private static string SiblingValue(IDictionary snapshot, string path, string sibling) + { + var parent = path.Contains('.') ? path[..(path.LastIndexOf('.') + 1)] : ""; + return snapshot != null && snapshot.TryGetValue(parent + sibling, out var value) ? value : null; + } + + private async Task Label(AuditReferenceAttribute reference, string owner, string id) + { + var key = (reference.Target, reference.List, owner, id); + if (!_names.TryGetValue(key, out var name)) + { + name = await NameOf(reference, owner, id); + _names[key] = name; + } + + return string.IsNullOrEmpty(name) ? id : $"{name} ({id})"; + } + + //the name in the snapshot of the referenced record, or of the element of its list + private async Task NameOf(AuditReferenceAttribute reference, string owner, string id) + { + var recordId = reference.List == null ? id : owner; + if (string.IsNullOrEmpty(recordId)) + return null; + + //only the field holding the name is read, not the whole record + var record = reference.List == null + ? await reader.Read(reference.Target, recordId, reference.Key, NameProperty(reference.Target)) + : await reader.Read(reference.Target, recordId, null, reference.List); + var path = reference.List == null + ? NameProperty(reference.Target) + : $"{reference.List}[id={id}].{NameProperty(ElementType(reference.Target.GetProperty(reference.List)!.PropertyType)!)}"; + return record != null && record.TryGetValue(path, out var name) && name != "" ? name : null; + } +} diff --git a/src/Core/Grand.Infrastructure/Audit/AuditTracker.cs b/src/Core/Grand.Infrastructure/Audit/AuditTracker.cs index 91265d7c77..8ff0970b95 100644 --- a/src/Core/Grand.Infrastructure/Audit/AuditTracker.cs +++ b/src/Core/Grand.Infrastructure/Audit/AuditTracker.cs @@ -3,7 +3,6 @@ using Grand.Domain.Audit; using Grand.Mediator; using Microsoft.Extensions.Logging; -using System.Collections.Concurrent; using System.Reflection; namespace Grand.Infrastructure.Audit; @@ -12,7 +11,11 @@ namespace Grand.Infrastructure.Audit; /// Scoped per request: snapshots each entity once before its first write, reads the "after" state in batches /// and writes the entries in one insert per batch. Does nothing until activated for a staff request. /// -public class AuditTracker(IAuditEntryWriter writer, IMediator mediator, ILogger logger) +public class AuditTracker( + IAuditEntryWriter writer, + IMediator mediator, + IAuditRecordReader recordReader, + ILogger logger) : IAuditTracker { public const int BatchSize = 200; @@ -27,9 +30,6 @@ public class AuditTracker(IAuditEntryWriter writer, IMediator mediator, ILogger< "LastActivityDateUtc", "LastIpAddress", "LastVisitedPage" ]; - //the property that names a record of each type: [AuditName], else Name - private static readonly ConcurrentDictionary NameProperties = new(); - private readonly List _bulk = []; private readonly Dictionary, Task>>>> @@ -180,7 +180,7 @@ public async Task FlushAsync() List entries; try { - entries = await BuildEntries(pending, loaders); + entries = await BuildEntries(pending, loaders, new AuditReferences(recordReader)); entries.AddRange(bulk); if (entries.Count == 0) return; @@ -215,7 +215,8 @@ public async Task FlushAsync() private static async Task> BuildEntries( List> pending, - Dictionary, Task>>>> loaders) + Dictionary, Task>>>> loaders, + AuditReferences references) { var entries = new List(); foreach (var group in pending.GroupBy(p => p.Key.Type)) @@ -232,6 +233,8 @@ private static async Task> BuildEntries( try { entry = Build(group.Key, item.Key.Id, item.Value, after); + if (entry != null) + await references.Describe(group.Key, entry.Changes, item.Value.Before, after); } catch (Exception) { @@ -291,8 +294,7 @@ private static AuditEntry Build(Type type, string entityId, Pending pending, private static string NameOf(Type type, IDictionary snapshot) { - var property = NameProperties.GetOrAdd(type, t => - t.GetProperties().FirstOrDefault(p => p.IsDefined(typeof(AuditNameAttribute)))?.Name ?? "Name"); + var property = AuditReferences.NameProperty(type); return snapshot != null && snapshot.TryGetValue(property, out var name) && name != "" ? name : null; } diff --git a/src/Core/Grand.Infrastructure/Startup/StartupApplication.cs b/src/Core/Grand.Infrastructure/Startup/StartupApplication.cs index ed25afb4e8..6a070a3929 100644 --- a/src/Core/Grand.Infrastructure/Startup/StartupApplication.cs +++ b/src/Core/Grand.Infrastructure/Startup/StartupApplication.cs @@ -82,6 +82,8 @@ private void RegisterDataLayer(IServiceCollection serviceCollection, IConfigurat serviceCollection.AddScoped(typeof(IRepository<>), typeof(AuditingMongoRepository<>)); serviceCollection.AddScoped(sp => new MongoAuditEntryWriter(sp.GetRequiredService)); + serviceCollection.AddScoped(sp => + new MongoAuditRecordReader(sp.GetRequiredService)); } else { @@ -93,6 +95,8 @@ private void RegisterDataLayer(IServiceCollection serviceCollection, IConfigurat serviceCollection.AddScoped(typeof(IRepository<>), typeof(AuditingLiteDBRepository<>)); serviceCollection.AddScoped(sp => new LiteDBAuditEntryWriter(sp.GetRequiredService)); + serviceCollection.AddScoped(sp => + new LiteDBAuditRecordReader(sp.GetRequiredService)); } serviceCollection.AddScoped(); diff --git a/src/Tests/Grand.Business.Common.Tests/Services/Audit/AuditServiceTests.cs b/src/Tests/Grand.Business.Common.Tests/Services/Audit/AuditServiceTests.cs index 954327d5f2..588a25e4de 100644 --- a/src/Tests/Grand.Business.Common.Tests/Services/Audit/AuditServiceTests.cs +++ b/src/Tests/Grand.Business.Common.Tests/Services/Audit/AuditServiceTests.cs @@ -75,16 +75,29 @@ public async Task GetEntries_FiltersByEntityNames() } [TestMethod] - public async Task GetEntries_ActorEmailContainsCaseInsensitive() + public async Task GetEntries_SearchText_StaffEmailCaseInsensitive() { var anna = await Add("Product", "p1", DateTime.UtcNow, "Anna@Shop.com"); await Add("Product", "p1", DateTime.UtcNow, "marek@shop.com"); - var result = await _service.GetEntries(new AuditEntrySearch { ActorEmail = "anna@" }); + var result = await _service.GetEntries(new AuditEntrySearch { SearchText = "anna@" }); Assert.AreEqual(anna.Id, result.Single().Id); } + [TestMethod] + public async Task GetEntries_SearchText_RecordName() + { + var lamp = await Add("Product", "p1", DateTime.UtcNow); + lamp.EntityName = "Aurora Lamp"; + await _repository.UpdateAsync(lamp); + await Add("Product", "p2", DateTime.UtcNow); + + var result = await _service.GetEntries(new AuditEntrySearch { SearchText = "aurora" }); + + Assert.AreEqual(lamp.Id, result.Single().Id); + } + [TestMethod] public async Task GetEntries_DateRange() { diff --git a/src/Tests/Grand.Infrastructure.Tests/Audit/AuditDiffTests.cs b/src/Tests/Grand.Infrastructure.Tests/Audit/AuditDiffTests.cs index c30de6e571..3266d4ed3b 100644 --- a/src/Tests/Grand.Infrastructure.Tests/Audit/AuditDiffTests.cs +++ b/src/Tests/Grand.Infrastructure.Tests/Audit/AuditDiffTests.cs @@ -145,6 +145,20 @@ public void Compare_KeyValueArray_AddedKey_ShowsValue() Assert.AreEqual("c9", changes.Single(c => c.Path == "UserFields[ImpersonatedCustomerId].Value").NewValue); } + [TestMethod] + public void Compare_KeyValueArray_SavedWithoutChanges_OnlyRealValues() + { + //a customer saved without edits: empty user fields dropped, a default one added + var before = new BsonDocument("UserFields", new BsonArray { UserField("FirstName", ""), UserField("LastName", "") }); + var after = new BsonDocument("UserFields", new BsonArray { UserField("TwoFactorEnabled", "False") }); + + var change = Compare(before, after).Single(); + + Assert.AreEqual("UserFields[TwoFactorEnabled].Value", change.Path); + Assert.IsNull(change.OldValue); + Assert.AreEqual("False", change.NewValue); + } + [TestMethod] public void Compare_KeyValueArray_SensitiveKey_Masked() { @@ -155,8 +169,8 @@ public void Compare_KeyValueArray_SensitiveKey_Masked() var changes = Compare(before, after); - //the changed token, and the Key and Value of the added secret - Assert.AreEqual(3, changes.Count); + //the changed token and the value of the added secret + Assert.AreEqual(2, changes.Count); foreach (var change in changes) { Assert.IsTrue(change.Masked, change.Path); @@ -192,6 +206,33 @@ public void Compare_LongValue_Truncated() Assert.IsTrue(changes[0].Truncated); } + [TestMethod] + public void Compare_DateDifferingInMilliseconds_Unchanged() + { + var stored = new DateTime(2026, 10, 26, 19, 34, 23, 497, DateTimeKind.Utc); + + Assert.AreEqual(0, Compare(new BsonDocument("StartDateUtc", stored), + new BsonDocument("StartDateUtc", stored.AddMilliseconds(-497))).Count); + Assert.AreEqual("2026-10-26T19:34:24Z", Compare(new BsonDocument("StartDateUtc", stored), + new BsonDocument("StartDateUtc", stored.AddSeconds(1))).Single().NewValue); + } + + [TestMethod] + public void Compare_Guid_ComparedAsValue() + { + var orderGuid = Guid.NewGuid(); + BsonDocument Order(Guid guid, string name) => new() { + { "OrderGuid", new BsonBinaryData(guid, GuidRepresentation.CSharpLegacy) }, { "FirstName", name } + }; + + var unchanged = Compare(Order(orderGuid, "Anna"), Order(orderGuid, "Ann")); + var changed = Compare(Order(orderGuid, "Anna"), Order(Guid.Empty, "Anna")); + + Assert.AreEqual("FirstName", unchanged.Single().Path); + Assert.AreEqual(orderGuid.ToString(), changed.Single().OldValue); + Assert.AreEqual(Guid.Empty.ToString(), changed.Single().NewValue); + } + [TestMethod] public void Compare_Binary_NotCompared_ListedAsChanged() { diff --git a/src/Tests/Grand.Infrastructure.Tests/Audit/AuditReferencesTests.cs b/src/Tests/Grand.Infrastructure.Tests/Audit/AuditReferencesTests.cs new file mode 100644 index 0000000000..f77fee3416 --- /dev/null +++ b/src/Tests/Grand.Infrastructure.Tests/Audit/AuditReferencesTests.cs @@ -0,0 +1,202 @@ +using Grand.Data.Audit; +using Grand.Domain.Audit; +using Grand.Domain.Catalog; +using Grand.Domain.Orders; +using Grand.Domain.Stores; +using Grand.Infrastructure.Audit; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Grand.Infrastructure.Tests.Audit; + +[TestClass] +public class AuditReferencesTests +{ + private readonly Dictionary<(Type, string), IDictionary> _records = new(); + private Mock _reader; + private AuditReferences _references; + + [TestInitialize] + public void Init() + { + _reader = new Mock(); + _reader.Setup(r => r.Read(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((type, value, key, _) => + Task.FromResult(_records.GetValueOrDefault((type, key == null ? value : $"{key}={value}")))); + _references = new AuditReferences(_reader.Object); + } + + private async Task Describe(Type type, string path, string oldValue, string newValue, + IDictionary after = null, bool truncated = false) + { + var change = new AuditChange { Path = path, OldValue = oldValue, NewValue = newValue, Truncated = truncated }; + await _references.Describe(type, [change], after, after); + return change; + } + + [TestMethod] + public async Task Reference_ShowsNameAndId() + { + _records[(typeof(Brand), "b1")] = new Dictionary { ["Name"] = "Aurora" }; + _records[(typeof(Brand), "b2")] = new Dictionary { ["Name"] = "Nordic" }; + + var change = await Describe(typeof(Product), "BrandId", "b1", "b2"); + + Assert.AreEqual("Aurora (b1)", change.OldValue); + Assert.AreEqual("Nordic (b2)", change.NewValue); + } + + [TestMethod] + public async Task ListElement_FoundInTheOwnerNamedByTheSibling() + { + _records[(typeof(SpecificationAttribute), "s1")] = new Dictionary { + ["Name"] = "Color", + ["SpecificationAttributeOptions[id=o1].Name"] = "Red", + ["SpecificationAttributeOptions[id=o2].Name"] = "Blue" + }; + var product = new Dictionary { ["ProductSpecificationAttributes[id=p1].SpecificationAttributeId"] = "s1" }; + + var change = await Describe(typeof(Product), "ProductSpecificationAttributes[id=p1].SpecificationAttributeOptionId", + "o1", "o2", product); + + Assert.AreEqual("Red (o1)", change.OldValue); + Assert.AreEqual("Blue (o2)", change.NewValue); + } + + [TestMethod] + public async Task AddressState_FoundInItsCountry() + { + _records[(typeof(Grand.Domain.Directory.Country), "pl")] = new Dictionary { + ["Name"] = "Poland", ["StateProvinces[id=st1].Name"] = "Mazowieckie" + }; + var order = new Dictionary { ["BillingAddress.CountryId"] = "pl" }; + + var country = await Describe(typeof(Order), "BillingAddress.CountryId", null, "pl", order); + var state = await Describe(typeof(Order), "BillingAddress.StateProvinceId", null, "st1", order); + + Assert.AreEqual("Poland (pl)", country.NewValue); + Assert.AreEqual("Mazowieckie (st1)", state.NewValue); + } + + [TestMethod] + public async Task ListOfIds_EachNamed() + { + _records[(typeof(Store), "s1")] = new Dictionary { ["Name"] = "Main" }; + + var change = await Describe(typeof(Product), "Stores", null, "[\"s1\", \"s9\"]"); + + Assert.AreEqual("Main (s1), s9", change.NewValue); + } + + [TestMethod] + public async Task ReferenceByKey_MatchesThatProperty() + { + _records[(typeof(OrderStatus), "StatusId=10")] = new Dictionary { ["Name"] = "Pending" }; + _records[(typeof(OrderStatus), "StatusId=40")] = new Dictionary { ["Name"] = "Cancelled" }; + + var change = await Describe(typeof(Order), "OrderStatusId", "10", "40"); + + Assert.AreEqual("Pending (10)", change.OldValue); + Assert.AreEqual("Cancelled (40)", change.NewValue); + } + + [TestMethod] + public async Task EnumProperty_ShowsName() + { + var change = await Describe(typeof(Order), "PaymentStatusId", "10", "30"); + + Assert.AreEqual("Pending (10)", change.OldValue); + Assert.AreEqual("Paid (30)", change.NewValue); + } + + //a wrong mark shows only ids with no sign of the mistake, so every mark of the domain is checked here + [TestMethod] + public void EveryReferenceMark_PointsToANamedRecord() + { + var problems = new List(); + foreach (var type in typeof(Grand.Domain.BaseEntity).Assembly.GetTypes()) + foreach (var property in type.GetProperties()) + { + if (property.GetCustomAttributes(typeof(AuditReferenceAttribute), false).FirstOrDefault() is not AuditReferenceAttribute mark) + continue; + + var name = $"{type.Name}.{property.Name}"; + var target = mark.Target; + var isList = property.PropertyType != typeof(string) + && typeof(System.Collections.IEnumerable).IsAssignableFrom(property.PropertyType); + if (target.IsEnum) + continue; + if (!typeof(Grand.Domain.BaseEntity).IsAssignableFrom(target)) + problems.Add($"{name}: {target.Name} is not an entity"); + if (property.PropertyType != typeof(string) && !isList && mark.Key == null) + problems.Add($"{name}: a {property.PropertyType.Name} value needs a Key"); + if (mark.Key != null && target.GetProperty(mark.Key) == null) + problems.Add($"{name}: {target.Name} has no {mark.Key}"); + if (mark.Owner != null && type.GetProperty(mark.Owner) == null) + problems.Add($"{name}: {type.Name} has no owner property {mark.Owner}"); + + var named = target; + if (mark.List != null) + { + var list = target.GetProperty(mark.List); + named = list?.PropertyType.GetGenericArguments().FirstOrDefault(); + if (named == null) + problems.Add($"{name}: {target.Name} has no list {mark.List}"); + if (mark.Owner == null) + problems.Add($"{name}: an element of a list needs an Owner"); + } + + if (named != null && named.GetProperty(AuditReferences.NameProperty(named)) == null) + problems.Add($"{name}: {named.Name} has no name property"); + } + + Assert.AreEqual(0, problems.Count, string.Join(Environment.NewLine, problems)); + } + + [TestMethod] + public async Task Reference_ReadsOnlyTheNameField() + { + _records[(typeof(Brand), "b1")] = new Dictionary { ["Name"] = "Aurora" }; + + await Describe(typeof(Product), "BrandId", null, "b1"); + + _reader.Verify(r => r.Read(typeof(Brand), "b1", null, "Name"), Times.Once); + } + + [TestMethod] + public async Task TruncatedValue_KeepsTheStoredValue() + { + _records[(typeof(Store), "s1")] = new Dictionary { ["Name"] = "Main" }; + + var change = await Describe(typeof(Product), "Stores", null, "[\"s1\", \"s", truncated: true); + + Assert.AreEqual("[\"s1\", \"s", change.NewValue); + Assert.IsTrue(change.Truncated); + _reader.Verify(r => r.Read(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [TestMethod] + public async Task LongNames_CutToTheLimitAndMarked() + { + var ids = Enumerable.Range(0, 40).Select(i => $"s{i}").ToList(); + foreach (var id in ids) + _records[(typeof(Store), id)] = new Dictionary { ["Name"] = new string('n', 40) }; + + var change = await Describe(typeof(Product), "Stores", null, + "[" + string.Join(", ", ids.Select(id => $"\"{id}\"")) + "]"); + + Assert.AreEqual(AuditDiff.MaxValueLength, change.NewValue.Length); + Assert.IsTrue(change.Truncated); + } + + [TestMethod] + public async Task UnmarkedOrMissing_KeepsTheStoredValue() + { + var unmarked = await Describe(typeof(Product), "Name", "a", "b"); + var missing = await Describe(typeof(Product), "BrandId", null, "b9"); + + Assert.AreEqual("b", unmarked.NewValue); + Assert.AreEqual("b9", missing.NewValue); + } +} diff --git a/src/Tests/Grand.Infrastructure.Tests/Audit/AuditTrackerTests.cs b/src/Tests/Grand.Infrastructure.Tests/Audit/AuditTrackerTests.cs index 809ab97836..a1f3265eaa 100644 --- a/src/Tests/Grand.Infrastructure.Tests/Audit/AuditTrackerTests.cs +++ b/src/Tests/Grand.Infrastructure.Tests/Audit/AuditTrackerTests.cs @@ -27,7 +27,8 @@ public void Init() { _writer = new CapturingWriter(); _mediator = new Mock(); - _tracker = new AuditTracker(_writer, _mediator.Object, NullLogger.Instance); + _tracker = new AuditTracker(_writer, _mediator.Object, new Mock().Object, + NullLogger.Instance); _access = ids => { _loadCalls++; diff --git a/src/Tests/Grand.Infrastructure.Tests/Audit/AuditingLiteDBRepositoryTests.cs b/src/Tests/Grand.Infrastructure.Tests/Audit/AuditingLiteDBRepositoryTests.cs index 779ee96548..0a6c705182 100644 --- a/src/Tests/Grand.Infrastructure.Tests/Audit/AuditingLiteDBRepositoryTests.cs +++ b/src/Tests/Grand.Infrastructure.Tests/Audit/AuditingLiteDBRepositoryTests.cs @@ -33,7 +33,8 @@ public void Init() writer.Setup(w => w.InsertManyAsync(It.IsAny>())) .Callback>(e => _written.AddRange(e)) .Returns(Task.CompletedTask); - _tracker = new AuditTracker(writer.Object, new Mock().Object, NullLogger.Instance); + _tracker = new AuditTracker(writer.Object, new Mock().Object, new Mock().Object, + NullLogger.Instance); var auditInfo = new Mock(); auditInfo.Setup(a => a.GetCurrentDateTime()).Returns(DateTime.UtcNow); _repository = new AuditingLiteDBRepository(_database, auditInfo.Object, _tracker); diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/AuditLogControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/AuditLogControllerTests.cs index e09c063490..a3d7525f4c 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/AuditLogControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/AuditLogControllerTests.cs @@ -88,11 +88,11 @@ public async Task List_Post_MapsFiltersToSearch() }, 0, 20)); var result = (JsonResult)await _controller.List(new DataSourceRequest { Page = 2, PageSize = 20 }, - new AuditLogListModel { EntityType = "Product", EntityId = "p1", ActorEmail = "anna", Kind = AuditKind.Update }); + new AuditLogListModel { EntityType = "Product", EntityId = "p1", SearchText = "anna", Kind = AuditKind.Update }); Assert.AreEqual("Product", search.EntityType); Assert.AreEqual("p1", search.EntityId); - Assert.AreEqual("anna", search.ActorEmail); + Assert.AreEqual("anna", search.SearchText); Assert.AreEqual(AuditKind.Update, search.Kind); var data = ((DataSourceResult)result.Value!).Data.Cast().Single(); Assert.AreEqual("Price", data.ChangedFields); diff --git a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseOrderManagementControllerTests.cs b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseOrderManagementControllerTests.cs index 723ed60c8d..5f872aa42d 100644 --- a/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseOrderManagementControllerTests.cs +++ b/src/Tests/Grand.Web.Admin.Tests/Controllers/BaseOrderManagementControllerTests.cs @@ -22,6 +22,7 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Grand.Web.Common.Audit; using Moq; using Grand.Web.Common.Localization; @@ -444,6 +445,20 @@ public async Task AddProductToOrderSearch_ScopeDenies_ReturnsGridError() Assert.IsNotNull(GridErrors(result)); } + [TestMethod] + public async Task OrderNoteAdd_RecordedOnTheOrder() + { + var order = new Order { Id = "o1", OrderNumber = 7, StoreId = "s1" }; + _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); + _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(true); + _orderNoteViewModelServiceMock.Setup(v => v.InsertOrderNote(order, null, false, "msg")).ReturnsAsync(true); + var recorder = new Mock(); + + await _controller.OrderNoteAdd("o1", null, false, "msg", recorder.Object); + + recorder.Verify(r => r.OwnedChange("Order", "o1", "7", "s1", "OrderNote", null, "msg"), Times.Once); + } + [TestMethod] public async Task OrderNoteAdd_FileRefused_ReturnsResultFalseWithMessage() { @@ -452,7 +467,7 @@ public async Task OrderNoteAdd_FileRefused_ReturnsResultFalseWithMessage() _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(true); _orderNoteViewModelServiceMock.Setup(v => v.InsertOrderNote(order, "d1", false, "msg")).ReturnsAsync(false); - var result = await _controller.OrderNoteAdd("o1", "d1", false, "msg"); + var result = await _controller.OrderNoteAdd("o1", "d1", false, "msg", new Mock().Object); var value = (result as JsonResult)?.Value; Assert.IsFalse((bool)value!.GetType().GetProperty("Result")!.GetValue(value)!); @@ -570,7 +585,7 @@ public async Task OrderNoteAdd_ScopeDenies_ReturnsJsonResultFalse() _orderServiceMock.Setup(s => s.GetOrderById("o1")).ReturnsAsync(order); _scopeMock.Setup(s => s.HasAccess(order)).ReturnsAsync(false); - var result = await _controller.OrderNoteAdd("o1", null, false, "msg"); + var result = await _controller.OrderNoteAdd("o1", null, false, "msg", new Mock().Object); var json = result as JsonResult; Assert.IsNotNull(json); diff --git a/src/Tests/Grand.Web.Common.Tests/Audit/AuditEventRecorderTests.cs b/src/Tests/Grand.Web.Common.Tests/Audit/AuditEventRecorderTests.cs index 6c9219c4df..9ff9238244 100644 --- a/src/Tests/Grand.Web.Common.Tests/Audit/AuditEventRecorderTests.cs +++ b/src/Tests/Grand.Web.Common.Tests/Audit/AuditEventRecorderTests.cs @@ -94,6 +94,28 @@ public async Task AccessDenied_RecordsPermissionAndAction() Assert.AreEqual("permission: ManagePlugins / Edit", entry.Details); } + [TestMethod] + public async Task OwnedChange_ScopedToTheOwnersStore() + { + await _recorder.OwnedChange("Order", "o1", "7", "s2", "OrderNote", null, "msg"); + + var entry = _recorded.Single(); + Assert.AreEqual(AuditKind.Update, entry.Kind); + Assert.AreEqual("o1", entry.EntityId); + Assert.AreEqual("s2", entry.EntityStoreId); + Assert.AreEqual("msg", entry.Changes.Single().NewValue); + } + + [TestMethod] + public async Task OwnedChange_LongNote_CutAndMarked() + { + await _recorder.OwnedChange("Order", "o1", "7", "s2", "OrderNote", null, new string('x', 3000)); + + var change = _recorded.Single().Changes.Single(); + Assert.AreEqual(AuditDiff.MaxValueLength, change.NewValue.Length); + Assert.IsTrue(change.Truncated); + } + [TestMethod] public async Task Plugin_RecordsNameAndChange() { diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/AuditLog/Detail.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/AuditLog/Detail.cshtml index 4baf101d5b..bd94acbc8a 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/AuditLog/Detail.cshtml +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/AuditLog/Detail.cshtml @@ -39,10 +39,13 @@
@Model.AppliesTo
} -
- @Loc["Admin.System.AuditLog.Fields.StoreId"] -
@entry.StoreId
-
+ @if (!string.IsNullOrEmpty(entry.StoreId)) + { +
+ @Loc["Admin.System.AuditLog.Fields.StoreId"] +
@entry.StoreId
+
+ }
@Loc["Admin.System.AuditLog.Fields.IpAddress"]
@entry.IpAddress
diff --git a/src/Web/Grand.Web.Admin/Areas/Admin/Views/AuditLog/List.cshtml b/src/Web/Grand.Web.Admin/Areas/Admin/Views/AuditLog/List.cshtml index d83f3bb7f4..18681fa93c 100644 --- a/src/Web/Grand.Web.Admin/Areas/Admin/Views/AuditLog/List.cshtml +++ b/src/Web/Grand.Web.Admin/Areas/Admin/Views/AuditLog/List.cshtml @@ -4,7 +4,7 @@ } @* The list shape without Add new: no primary action, Export CSV as an outline button in the header, - the actor e-mail as the quick search with the other criteria behind the Filters toggle, and the + the staff e-mail or record name as the quick search with the other criteria behind the Filters toggle, and the grid in a flush card. The export posts the same filter form the grid reads. *@ @@ -16,7 +16,7 @@
- +
@@ -35,6 +35,7 @@ + {{ CreatedOn }} @@ -45,6 +46,13 @@ + + + + {{ $texts.view }} + + + @@ -55,7 +63,7 @@ GrandAdmin.grids.get('#auditlog-grid').dataSource.page(1); return false; }); - $("#@Html.IdFor(model => model.ActorEmail)").keydown(function (event) { + $("#@Html.IdFor(model => model.SearchText)").keydown(function (event) { if (event.keyCode == 13) { $("#search-auditlog").click(); return false; @@ -67,7 +75,7 @@ var data = { FromUtc: $('#@Html.IdFor(model => model.FromUtc)').val(), ToUtc: $('#@Html.IdFor(model => model.ToUtc)').val(), - ActorEmail: $('#@Html.IdFor(model => model.ActorEmail)').val(), + SearchText: $('#@Html.IdFor(model => model.SearchText)').val(), EntityType: $('#@Html.IdFor(model => model.EntityType)').val(), EntityId: $('#@Html.IdFor(model => model.EntityId)').val(), Kind: $('#@Html.IdFor(model => model.Kind)').val(), diff --git a/src/Web/Grand.Web.Admin/Controllers/AuditLogController.cs b/src/Web/Grand.Web.Admin/Controllers/AuditLogController.cs index 7263468a5f..f0d366d664 100644 --- a/src/Web/Grand.Web.Admin/Controllers/AuditLogController.cs +++ b/src/Web/Grand.Web.Admin/Controllers/AuditLogController.cs @@ -177,7 +177,7 @@ private static AuditEntrySearch ToSearch(AuditLogListModel model) return new AuditEntrySearch { FromUtc = AsUtc(model.FromUtc), ToUtc = AsUtc(model.ToUtc?.Date.AddDays(1).AddTicks(-1)), - ActorEmail = model.ActorEmail?.Trim(), + SearchText = model.SearchText?.Trim(), Source = model.Source, Kind = model.Kind, EntityType = model.EntityType, @@ -274,7 +274,7 @@ void Add(string name, object value) Add("from", model.FromUtc?.ToString("yyyy-MM-dd")); Add("to", model.ToUtc?.ToString("yyyy-MM-dd")); - Add("actor", model.ActorEmail); + Add("search", model.SearchText); Add("source", model.Source); Add("kind", model.Kind); Add("entity type", model.EntityType); diff --git a/src/Web/Grand.Web.Admin/Models/AuditLog/AuditLogListModel.cs b/src/Web/Grand.Web.Admin/Models/AuditLog/AuditLogListModel.cs index 8310cf6d67..6c50bb0db6 100644 --- a/src/Web/Grand.Web.Admin/Models/AuditLog/AuditLogListModel.cs +++ b/src/Web/Grand.Web.Admin/Models/AuditLog/AuditLogListModel.cs @@ -16,8 +16,8 @@ public class AuditLogListModel : BaseModel [UIHint("DateNullable")] public DateTime? ToUtc { get; set; } - [GrandResourceDisplayName("Admin.System.AuditLog.Fields.ActorEmail")] - public string ActorEmail { get; set; } + [GrandResourceDisplayName("Admin.System.AuditLog.Fields.SearchText")] + public string SearchText { get; set; } [GrandResourceDisplayName("Admin.System.AuditLog.Fields.Source")] public AuditSource? Source { get; set; } diff --git a/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderManagementController.cs b/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderManagementController.cs index 7c63ef23e0..76e35b8019 100644 --- a/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderManagementController.cs +++ b/src/Web/Grand.Web.AdminShared/Controllers/BaseOrderManagementController.cs @@ -6,6 +6,7 @@ using Grand.Business.Core.Utilities.Catalog; using Grand.Domain.Catalog; using Grand.Domain.Orders; +using Grand.Web.Common.Audit; using Grand.Domain.Permissions; using Grand.Infrastructure; using Grand.Mediator; @@ -659,7 +660,8 @@ public async Task OrderNotesSelect(string orderId, Grand.Web.Comm [PermissionAuthorizeAction(PermissionActionName.Edit)] [HttpPost] - public async Task OrderNoteAdd(string orderId, string downloadId, bool displayToCustomer, string message) + public async Task OrderNoteAdd(string orderId, string downloadId, bool displayToCustomer, string message, + [FromServices] IAuditEventRecorder auditEventRecorder) { var order = await OrderService.GetOrderById(orderId); if (order == null || !await Scope.HasAccess(order)) return Json(new { Result = false }); @@ -670,18 +672,25 @@ public async Task OrderNoteAdd(string orderId, string downloadId, Message = TranslationService.GetResource("Admin.Orders.OrderNotes.DownloadNotOfOrder") }); + //a note is a record of its own; this puts it in the order's change history + await auditEventRecorder.OwnedChange(nameof(Order), order.Id, order.OrderNumber.ToString(), order.StoreId, + "OrderNote", null, message); return Json(new { Result = true }); } [PermissionAuthorizeAction(PermissionActionName.Delete)] [HttpPost] - public async Task OrderNoteDelete(string id, string orderId) + public async Task OrderNoteDelete(string id, string orderId, + [FromServices] IAuditEventRecorder auditEventRecorder) { var order = await OrderService.GetOrderById(orderId) ?? throw new ArgumentException("No order found with the specified id"); if (!await Scope.HasAccess(order)) return Json(new { Result = false }); + var note = (await OrderService.GetOrderNotes(order.Id)).FirstOrDefault(n => n.Id == id); await orderNoteViewModelService.DeleteOrderNote(order, id); + await auditEventRecorder.OwnedChange(nameof(Order), order.Id, order.OrderNumber.ToString(), order.StoreId, + "OrderNote", note?.Note, null); return new JsonResult(""); } diff --git a/src/Web/Grand.Web.Common/Audit/AuditEventRecorder.cs b/src/Web/Grand.Web.Common/Audit/AuditEventRecorder.cs index 4d566a9992..5658d16f5e 100644 --- a/src/Web/Grand.Web.Common/Audit/AuditEventRecorder.cs +++ b/src/Web/Grand.Web.Common/Audit/AuditEventRecorder.cs @@ -3,6 +3,7 @@ using Grand.Domain.Audit; using Grand.Domain.Customers; using Grand.Infrastructure; +using Grand.Infrastructure.Audit; using Grand.Infrastructure.Configuration; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -88,6 +89,19 @@ public Task Plugin(string pluginName, string change) }); } + public Task OwnedChange(string entityType, string entityId, string entityName, string entityStoreId, string path, + string oldValue, string newValue) + { + return Record(AuditKind.Update, CurrentStaff(), entry => + { + entry.EntityType = entityType; + entry.EntityId = entityId; + entry.EntityName = entityName; + entry.EntityStoreId = entityStoreId; + entry.Changes = [AuditDiff.Change(path, oldValue, newValue)]; + }); + } + private async Task RecordSignInEvent(AuditKind kind, Customer customer, string reason) { try diff --git a/src/Web/Grand.Web.Common/Audit/IAuditEventRecorder.cs b/src/Web/Grand.Web.Common/Audit/IAuditEventRecorder.cs index 551471153c..20af841f09 100644 --- a/src/Web/Grand.Web.Common/Audit/IAuditEventRecorder.cs +++ b/src/Web/Grand.Web.Common/Audit/IAuditEventRecorder.cs @@ -40,4 +40,11 @@ public interface IAuditEventRecorder /// Records a plugin installed, uninstalled, removed or uploaded by the current staff member ///
Task Plugin(string pluginName, string change); + + /// + /// Records, on a record, a change made through a separate record that belongs to it (a note of an order), so it + /// shows in the owner's change history + /// + Task OwnedChange(string entityType, string entityId, string entityName, string entityStoreId, string path, + string oldValue, string newValue); } diff --git a/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml b/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml index 2c6155bd59..d6b2e3692a 100644 Binary files a/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml and b/src/Web/Grand.Web/App_Data/Resources/DefaultLanguage.xml differ diff --git a/src/Web/Grand.Web/App_Data/Resources/Upgrade/en_250.xml b/src/Web/Grand.Web/App_Data/Resources/Upgrade/en_250.xml index cf98ef9c9a..82a59aa1c3 100644 --- a/src/Web/Grand.Web/App_Data/Resources/Upgrade/en_250.xml +++ b/src/Web/Grand.Web/App_Data/Resources/Upgrade/en_250.xml @@ -516,6 +516,9 @@ Who + + Staff e-mail or record name + Panel