diff --git a/Directory.Packages.props b/Directory.Packages.props
index 5f787c5f7..667b2ed52 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -30,7 +30,6 @@
-
@@ -59,10 +58,11 @@
-
-
+
+
+
diff --git a/GrandNode.sln b/GrandNode.sln
index c6bd60b9f..20ae11d7c 100644
--- a/GrandNode.sln
+++ b/GrandNode.sln
@@ -39,6 +39,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grand.Web.Common", "src\Web
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grand.Infrastructure", "src\Core\Grand.Infrastructure\Grand.Infrastructure.csproj", "{34A46D97-5996-46B3-BDCA-631EDAA0E210}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grand.Mapping", "src\Core\Grand.Mapping\Grand.Mapping.csproj", "{D4E5F6A7-B8C9-4D0E-A236-B4C5D6E7F8A9}"
+EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{F6F48227-B3C5-4A51-B33F-FF9AD96352DB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Authentication.Facebook", "src\Plugins\Authentication.Facebook\Authentication.Facebook.csproj", "{BA4579AF-9C20-477A-891B-926968B11C68}"
@@ -145,6 +147,16 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.ServiceDefaults", "s
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Grand.Web.AdminShared", "src\Web\Grand.Web.AdminShared\Grand.Web.AdminShared.csproj", "{12C4A556-E62E-4EC0-BCD9-E9D4E1F3D430}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72D-47B6-A68D-7590B98EB39B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Grand.Mapping.Tests", "src\Tests\Grand.Mapping.Tests\Grand.Mapping.Tests.csproj", "{396E6929-5365-4116-8AA8-DF5327247798}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Web", "Web", "{03997797-E7F5-0643-168D-B8EA7178C2FE}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Modules", "Modules", "{EC447DCF-ABFA-6E24-52A5-D7FD48A5C558}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Business", "Business", "{CC1F4FA2-92F2-BA4B-815B-7EC2CBBCCF50}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -323,6 +335,18 @@ Global
{34A46D97-5996-46B3-BDCA-631EDAA0E210}.Release|x64.Build.0 = Release|Any CPU
{34A46D97-5996-46B3-BDCA-631EDAA0E210}.Release|x86.ActiveCfg = Release|Any CPU
{34A46D97-5996-46B3-BDCA-631EDAA0E210}.Release|x86.Build.0 = Release|Any CPU
+ {D4E5F6A7-B8C9-4D0E-A236-B4C5D6E7F8A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D4E5F6A7-B8C9-4D0E-A236-B4C5D6E7F8A9}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D4E5F6A7-B8C9-4D0E-A236-B4C5D6E7F8A9}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D4E5F6A7-B8C9-4D0E-A236-B4C5D6E7F8A9}.Debug|x64.Build.0 = Debug|Any CPU
+ {D4E5F6A7-B8C9-4D0E-A236-B4C5D6E7F8A9}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D4E5F6A7-B8C9-4D0E-A236-B4C5D6E7F8A9}.Debug|x86.Build.0 = Debug|Any CPU
+ {D4E5F6A7-B8C9-4D0E-A236-B4C5D6E7F8A9}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D4E5F6A7-B8C9-4D0E-A236-B4C5D6E7F8A9}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D4E5F6A7-B8C9-4D0E-A236-B4C5D6E7F8A9}.Release|x64.ActiveCfg = Release|Any CPU
+ {D4E5F6A7-B8C9-4D0E-A236-B4C5D6E7F8A9}.Release|x64.Build.0 = Release|Any CPU
+ {D4E5F6A7-B8C9-4D0E-A236-B4C5D6E7F8A9}.Release|x86.ActiveCfg = Release|Any CPU
+ {D4E5F6A7-B8C9-4D0E-A236-B4C5D6E7F8A9}.Release|x86.Build.0 = Release|Any CPU
{BA4579AF-9C20-477A-891B-926968B11C68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BA4579AF-9C20-477A-891B-926968B11C68}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BA4579AF-9C20-477A-891B-926968B11C68}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -863,6 +887,18 @@ Global
{12C4A556-E62E-4EC0-BCD9-E9D4E1F3D430}.Release|x64.Build.0 = Release|Any CPU
{12C4A556-E62E-4EC0-BCD9-E9D4E1F3D430}.Release|x86.ActiveCfg = Release|Any CPU
{12C4A556-E62E-4EC0-BCD9-E9D4E1F3D430}.Release|x86.Build.0 = Release|Any CPU
+ {396E6929-5365-4116-8AA8-DF5327247798}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {396E6929-5365-4116-8AA8-DF5327247798}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {396E6929-5365-4116-8AA8-DF5327247798}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {396E6929-5365-4116-8AA8-DF5327247798}.Debug|x64.Build.0 = Debug|Any CPU
+ {396E6929-5365-4116-8AA8-DF5327247798}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {396E6929-5365-4116-8AA8-DF5327247798}.Debug|x86.Build.0 = Debug|Any CPU
+ {396E6929-5365-4116-8AA8-DF5327247798}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {396E6929-5365-4116-8AA8-DF5327247798}.Release|Any CPU.Build.0 = Release|Any CPU
+ {396E6929-5365-4116-8AA8-DF5327247798}.Release|x64.ActiveCfg = Release|Any CPU
+ {396E6929-5365-4116-8AA8-DF5327247798}.Release|x64.Build.0 = Release|Any CPU
+ {396E6929-5365-4116-8AA8-DF5327247798}.Release|x86.ActiveCfg = Release|Any CPU
+ {396E6929-5365-4116-8AA8-DF5327247798}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -882,6 +918,7 @@ Global
{D2E8AC42-2751-4AF9-87E4-54A8B2034360} = {6EAC4D2C-4A86-4C9F-8427-CB374F69F08C}
{1A6A0104-F610-4DD3-88BE-C4029034E5DA} = {BF4543A8-0731-4FDD-BB6B-0ADB9561F981}
{34A46D97-5996-46B3-BDCA-631EDAA0E210} = {FA350BD6-C29D-40D9-BA47-FE5FBDFC84A0}
+ {D4E5F6A7-B8C9-4D0E-A236-B4C5D6E7F8A9} = {FA350BD6-C29D-40D9-BA47-FE5FBDFC84A0}
{BA4579AF-9C20-477A-891B-926968B11C68} = {F6F48227-B3C5-4A51-B33F-FF9AD96352DB}
{6F24BF9C-EA4E-42AB-A9A8-435CE69362B8} = {F6F48227-B3C5-4A51-B33F-FF9AD96352DB}
{64775F93-5A04-495F-97C6-BDB3835B18F6} = {F6F48227-B3C5-4A51-B33F-FF9AD96352DB}
@@ -926,6 +963,10 @@ Global
{E0B26803-010B-4198-8016-002242076B61} = {583FFBBD-421C-4FB7-BBDA-F9CAF35A354A}
{136F1E35-8B20-465C-8D42-30A5A0D0DA1F} = {583FFBBD-421C-4FB7-BBDA-F9CAF35A354A}
{12C4A556-E62E-4EC0-BCD9-E9D4E1F3D430} = {BF4543A8-0731-4FDD-BB6B-0ADB9561F981}
+ {396E6929-5365-4116-8AA8-DF5327247798} = {6360202A-F931-4BBD-ADBD-C9A628EE59F8}
+ {03997797-E7F5-0643-168D-B8EA7178C2FE} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {EC447DCF-ABFA-6E24-52A5-D7FD48A5C558} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {CC1F4FA2-92F2-BA4B-815B-7EC2CBBCCF50} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {88B478F4-FD3B-4C24-9E84-4FAAF0254397}
diff --git a/src/Business/Grand.Business.Catalog/Services/ExportImport/Mapper/BrandProfile.cs b/src/Business/Grand.Business.Catalog/Services/ExportImport/Mapper/BrandProfile.cs
index e2dbaca30..090937991 100644
--- a/src/Business/Grand.Business.Catalog/Services/ExportImport/Mapper/BrandProfile.cs
+++ b/src/Business/Grand.Business.Catalog/Services/ExportImport/Mapper/BrandProfile.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Business.Core.Dto;
using Grand.Domain.Catalog;
using Grand.Infrastructure.Mapper;
diff --git a/src/Business/Grand.Business.Catalog/Services/ExportImport/Mapper/CategoryProfile.cs b/src/Business/Grand.Business.Catalog/Services/ExportImport/Mapper/CategoryProfile.cs
index 383bfe4a1..3659da20c 100644
--- a/src/Business/Grand.Business.Catalog/Services/ExportImport/Mapper/CategoryProfile.cs
+++ b/src/Business/Grand.Business.Catalog/Services/ExportImport/Mapper/CategoryProfile.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Business.Core.Dto;
using Grand.Domain.Catalog;
using Grand.Infrastructure.Mapper;
diff --git a/src/Business/Grand.Business.Catalog/Services/ExportImport/Mapper/CollectionProfile.cs b/src/Business/Grand.Business.Catalog/Services/ExportImport/Mapper/CollectionProfile.cs
index 0e72900d6..21356d7cf 100644
--- a/src/Business/Grand.Business.Catalog/Services/ExportImport/Mapper/CollectionProfile.cs
+++ b/src/Business/Grand.Business.Catalog/Services/ExportImport/Mapper/CollectionProfile.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Business.Core.Dto;
using Grand.Domain.Catalog;
using Grand.Infrastructure.Mapper;
diff --git a/src/Business/Grand.Business.Catalog/Services/ExportImport/Mapper/ProductProfile.cs b/src/Business/Grand.Business.Catalog/Services/ExportImport/Mapper/ProductProfile.cs
index d6fe0798c..dd5a3a775 100644
--- a/src/Business/Grand.Business.Catalog/Services/ExportImport/Mapper/ProductProfile.cs
+++ b/src/Business/Grand.Business.Catalog/Services/ExportImport/Mapper/ProductProfile.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Business.Core.Dto;
using Grand.Domain.Catalog;
using Grand.Infrastructure.Mapper;
diff --git a/src/Core/Grand.Infrastructure/Grand.Infrastructure.csproj b/src/Core/Grand.Infrastructure/Grand.Infrastructure.csproj
index 806f5ef2f..c2d4600b1 100644
--- a/src/Core/Grand.Infrastructure/Grand.Infrastructure.csproj
+++ b/src/Core/Grand.Infrastructure/Grand.Infrastructure.csproj
@@ -8,7 +8,6 @@
-
@@ -20,5 +19,6 @@
+
\ No newline at end of file
diff --git a/src/Core/Grand.Infrastructure/Mapper/AutoMapperConfig.cs b/src/Core/Grand.Infrastructure/Mapper/AutoMapperConfig.cs
index 9d32a43de..48d56acef 100644
--- a/src/Core/Grand.Infrastructure/Mapper/AutoMapperConfig.cs
+++ b/src/Core/Grand.Infrastructure/Mapper/AutoMapperConfig.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
namespace Grand.Infrastructure.Mapper;
diff --git a/src/Core/Grand.Infrastructure/Mapper/MappingExtensions.cs b/src/Core/Grand.Infrastructure/Mapper/MappingExtensions.cs
index 9f0fc981a..806e0676e 100644
--- a/src/Core/Grand.Infrastructure/Mapper/MappingExtensions.cs
+++ b/src/Core/Grand.Infrastructure/Mapper/MappingExtensions.cs
@@ -2,7 +2,7 @@
public static class MappingExtensions
{
- public static TDestination MapTo(this TSource source)
+ public static TDestination MapTo(this TSource source) where TDestination : new()
{
return AutoMapperConfig.Mapper.Map(source);
}
diff --git a/src/Core/Grand.Infrastructure/StartupBase.cs b/src/Core/Grand.Infrastructure/StartupBase.cs
index 33f1c35d8..34b6dc411 100644
--- a/src/Core/Grand.Infrastructure/StartupBase.cs
+++ b/src/Core/Grand.Infrastructure/StartupBase.cs
@@ -1,5 +1,5 @@
-using AutoMapper;
-using Grand.Data;
+using Grand.Data;
+using Grand.Mapping;
using Grand.Infrastructure.Configuration;
using Grand.Infrastructure.Extensions;
using Grand.Infrastructure.Mapper;
@@ -67,7 +67,7 @@ private static void InitAutoMapper(ITypeSearcher typeSearcher)
//create AutoMapper configuration
var config = new MapperConfiguration(cfg =>
{
- foreach (var instance in instances) cfg.AddProfile(instance.GetType());
+ foreach (var instance in instances) cfg.AddProfile((Grand.Mapping.Profile)instance);
});
//register automapper
diff --git a/src/Core/Grand.Mapping/Grand.Mapping.csproj b/src/Core/Grand.Mapping/Grand.Mapping.csproj
new file mode 100644
index 000000000..b9b0d198f
--- /dev/null
+++ b/src/Core/Grand.Mapping/Grand.Mapping.csproj
@@ -0,0 +1,3 @@
+
+
+
diff --git a/src/Core/Grand.Mapping/GrandMapper.cs b/src/Core/Grand.Mapping/GrandMapper.cs
new file mode 100644
index 000000000..08771d597
--- /dev/null
+++ b/src/Core/Grand.Mapping/GrandMapper.cs
@@ -0,0 +1,25 @@
+using System.Collections.Frozen;
+
+namespace Grand.Mapping;
+
+internal sealed class GrandMapper : IMapper
+{
+ private readonly FrozenDictionary<(Type, Type), Delegate> _mappings;
+
+ internal GrandMapper(Dictionary<(Type, Type), Delegate> mappings)
+ => _mappings = mappings.ToFrozenDictionary();
+
+ public TDest Map(TSource source) where TDest : new()
+ {
+ return Map(source, new TDest());
+ }
+
+ public TDest Map(TSource source, TDest destination)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ ArgumentNullException.ThrowIfNull(destination);
+ if (_mappings.TryGetValue((typeof(TSource), typeof(TDest)), out var del))
+ ((Action)del)(source, destination);
+ return destination;
+ }
+}
diff --git a/src/Core/Grand.Mapping/IMapper.cs b/src/Core/Grand.Mapping/IMapper.cs
new file mode 100644
index 000000000..9809f155d
--- /dev/null
+++ b/src/Core/Grand.Mapping/IMapper.cs
@@ -0,0 +1,7 @@
+namespace Grand.Mapping;
+
+public interface IMapper
+{
+ TDest Map(TSource source) where TDest : new();
+ TDest Map(TSource source, TDest destination);
+}
diff --git a/src/Core/Grand.Mapping/IMapperConfigurationExpression.cs b/src/Core/Grand.Mapping/IMapperConfigurationExpression.cs
new file mode 100644
index 000000000..de4d21743
--- /dev/null
+++ b/src/Core/Grand.Mapping/IMapperConfigurationExpression.cs
@@ -0,0 +1,7 @@
+namespace Grand.Mapping;
+
+public interface IMapperConfigurationExpression
+{
+ void AddProfile(Profile profile);
+ void AddProfile() where T : Profile, new() => AddProfile(new T());
+}
diff --git a/src/Core/Grand.Mapping/IMappingExpression.cs b/src/Core/Grand.Mapping/IMappingExpression.cs
new file mode 100644
index 000000000..8ee91b5a2
--- /dev/null
+++ b/src/Core/Grand.Mapping/IMappingExpression.cs
@@ -0,0 +1,14 @@
+using System.Linq.Expressions;
+
+namespace Grand.Mapping;
+
+public interface IMappingExpression
+{
+ IMappingExpression ForMember(
+ Expression> destMember,
+ Action> opts);
+
+ IMappingExpression ForPath(
+ Expression> destPath,
+ Action> opts);
+}
diff --git a/src/Core/Grand.Mapping/IMemberConfigurationExpression.cs b/src/Core/Grand.Mapping/IMemberConfigurationExpression.cs
new file mode 100644
index 000000000..2e73fc566
--- /dev/null
+++ b/src/Core/Grand.Mapping/IMemberConfigurationExpression.cs
@@ -0,0 +1,10 @@
+using System.Linq.Expressions;
+
+namespace Grand.Mapping;
+
+public interface IMemberConfigurationExpression
+{
+ void Ignore();
+ void MapFrom(Expression> mapExpression);
+ void Condition(Expression> condition);
+}
diff --git a/src/Core/Grand.Mapping/Internal/IMappingConfiguration.cs b/src/Core/Grand.Mapping/Internal/IMappingConfiguration.cs
new file mode 100644
index 000000000..5a9aab102
--- /dev/null
+++ b/src/Core/Grand.Mapping/Internal/IMappingConfiguration.cs
@@ -0,0 +1,7 @@
+namespace Grand.Mapping.Internal;
+
+internal interface IMappingConfiguration
+{
+ (Type Source, Type Dest) GetTypes();
+ Delegate CompileDelegate(HashSet<(Type, Type)> registeredTypes, Dictionary<(Type, Type), Delegate> mappings);
+}
diff --git a/src/Core/Grand.Mapping/Internal/MapperConfigurationExpressionImpl.cs b/src/Core/Grand.Mapping/Internal/MapperConfigurationExpressionImpl.cs
new file mode 100644
index 000000000..f107add3f
--- /dev/null
+++ b/src/Core/Grand.Mapping/Internal/MapperConfigurationExpressionImpl.cs
@@ -0,0 +1,11 @@
+namespace Grand.Mapping.Internal;
+
+internal sealed class MapperConfigurationExpressionImpl : IMapperConfigurationExpression
+{
+ private readonly List _configurations = new();
+
+ public void AddProfile(Profile profile)
+ => _configurations.AddRange(profile.GetConfigurations());
+
+ internal IEnumerable GetConfigurations() => _configurations;
+}
diff --git a/src/Core/Grand.Mapping/Internal/MappingCompiler.cs b/src/Core/Grand.Mapping/Internal/MappingCompiler.cs
new file mode 100644
index 000000000..3cb4bde0c
--- /dev/null
+++ b/src/Core/Grand.Mapping/Internal/MappingCompiler.cs
@@ -0,0 +1,373 @@
+#nullable enable
+
+using System.Linq.Expressions;
+using System.Reflection;
+
+namespace Grand.Mapping.Internal;
+
+internal static class MappingCompiler
+{
+ private static readonly MethodInfo _dictGetItemMethod =
+ typeof(Dictionary<(Type, Type), Delegate>).GetMethod("get_Item")!;
+
+ public static Action Compile(
+ List configs,
+ HashSet<(Type, Type)> registeredTypes,
+ Dictionary<(Type, Type), Delegate> mappings)
+ {
+ var src = Expression.Parameter(typeof(TSource), "src");
+ var dst = Expression.Parameter(typeof(TDest), "dst");
+ var body = new List();
+
+ var direct = BuildDirectConfigLookup(configs);
+
+ foreach (var dp in typeof(TDest)
+ .GetProperties(BindingFlags.Public | BindingFlags.Instance)
+ .Where(p => p.CanWrite))
+ {
+ var destAccess = Expression.Property(dst, dp);
+ if (direct.TryGetValue(dp.Name, out var mc))
+ ProcessMappedProperty(body, dp, mc, src, destAccess, registeredTypes, mappings);
+ else
+ ProcessAutoProperty(body, dp, src, destAccess, registeredTypes, mappings);
+ }
+
+ ProcessForPaths(body, configs, src, dst);
+
+ return Expression.Lambda>(
+ body.Count > 0 ? (Expression)Expression.Block(body) : Expression.Empty(),
+ src, dst).Compile();
+ }
+
+ private static Dictionary BuildDirectConfigLookup(List configs)
+ {
+ var direct = new Dictionary(StringComparer.Ordinal);
+ foreach (var c in configs.Where(c => !c.IsPath))
+ direct[c.MemberName] = c;
+ return direct;
+ }
+
+ private static void ProcessMappedProperty(
+ List body, PropertyInfo dp, MemberConfig mc,
+ ParameterExpression src, Expression destAccess,
+ HashSet<(Type, Type)> registeredTypes,
+ Dictionary<(Type, Type), Delegate> mappings)
+ {
+ if (mc.IsIgnored) return;
+
+ var value = mc.MapFromExpression != null
+ ? InlineLambda(mc.MapFromExpression, src)
+ : SourceProp(src, dp.Name);
+
+ if (value == null) return;
+
+ var expr = BuildAssignOrNested(value, destAccess, dp.PropertyType,
+ mc.ConditionExpression, src, registeredTypes, mappings);
+ if (expr != null) body.Add(expr);
+ }
+
+ private static void ProcessAutoProperty(
+ List body, PropertyInfo dp,
+ ParameterExpression src, Expression destAccess,
+ HashSet<(Type, Type)> registeredTypes,
+ Dictionary<(Type, Type), Delegate> mappings)
+ {
+ var srcExpr = SourceProp(src, dp.Name);
+ if (srcExpr == null) return;
+
+ var expr = BuildAssignOrNested(srcExpr, destAccess, dp.PropertyType,
+ null, src, registeredTypes, mappings);
+ if (expr != null) body.Add(expr);
+ }
+
+ private static Expression? BuildAssignOrNested(
+ Expression value, Expression destAccess, Type destType,
+ LambdaExpression? condition, ParameterExpression src,
+ HashSet<(Type, Type)> registeredTypes,
+ Dictionary<(Type, Type), Delegate> mappings)
+ {
+ var coerced = Coerce(value, destType);
+ if (coerced != null)
+ return WrapWithCondition(Expression.Assign(destAccess, coerced), condition, src);
+
+ return BuildNestedMapping(value, destAccess, destType, condition, src, registeredTypes, mappings);
+ }
+
+ private static void ProcessForPaths(
+ List body, List configs,
+ ParameterExpression src, ParameterExpression dst)
+ {
+ foreach (var pc in configs.Where(c => c.IsPath && !c.IsIgnored
+ && c.MapFromExpression != null && c.DestinationPathExpression != null))
+ {
+ var expr = BuildForPathExpression(pc, src, dst);
+ if (expr != null) body.Add(expr);
+ }
+ }
+
+ private static Expression? BuildForPathExpression(
+ MemberConfig pc, ParameterExpression src, ParameterExpression dst)
+ {
+ var destAccess = SubstitutePath(pc.DestinationPathExpression!, dst);
+ if (destAccess == null) return null;
+
+ var value = Coerce(InlineLambda(pc.MapFromExpression!, src), destAccess.Type);
+ if (value == null) return null;
+
+ return WrapWithCondition(Expression.Assign(destAccess, value), pc.ConditionExpression, src);
+ }
+
+ private static Expression WrapWithCondition(
+ Expression expr, LambdaExpression? condition, ParameterExpression src)
+ => condition != null
+ ? Expression.IfThen(InlineLambda(condition, src), expr)
+ : expr;
+
+ private static Expression? BuildNestedMapping(
+ Expression srcValueExpr,
+ Expression destAccess,
+ Type destType,
+ LambdaExpression? condition,
+ ParameterExpression srcParam,
+ HashSet<(Type, Type)> registeredTypes,
+ Dictionary<(Type, Type), Delegate> mappings)
+ {
+ var srcType = srcValueExpr.Type;
+
+ if (IsDirectObjectMapping(srcType, destType, registeredTypes))
+ {
+ var innerBlock = BuildDirectObjectMappingBlock(
+ srcValueExpr, destAccess, srcType, destType, mappings);
+ return WrapWithCondition(innerBlock, condition, srcParam);
+ }
+
+ var srcElem = CollectionElementType(srcType);
+ var dstElem = CollectionElementType(destType);
+ if (srcElem != null && dstElem != null && srcElem != dstElem
+ && registeredTypes.Contains((srcElem, dstElem)))
+ {
+ var converted = BuildCrossTypeCollectionCoerce(srcValueExpr, destType, srcElem, dstElem, mappings);
+ return WrapWithCondition(Expression.Assign(destAccess, converted), condition, srcParam);
+ }
+
+ return null;
+ }
+
+ private static bool IsDirectObjectMapping(
+ Type srcType, Type destType, HashSet<(Type, Type)> registeredTypes)
+ => !srcType.IsValueType && !destType.IsValueType
+ && CollectionElementType(srcType) == null
+ && CollectionElementType(destType) == null
+ && destType.GetConstructor(Type.EmptyTypes) != null
+ && registeredTypes.Contains((srcType, destType));
+
+ private static Expression BuildDirectObjectMappingBlock(
+ Expression srcValueExpr, Expression destAccess,
+ Type srcType, Type destType,
+ Dictionary<(Type, Type), Delegate> mappings)
+ {
+ var castDel = Expression.Convert(
+ Expression.Call(Expression.Constant(mappings), _dictGetItemMethod,
+ Expression.Constant((srcType, destType))),
+ typeof(Action<,>).MakeGenericType(srcType, destType));
+
+ var srcVar = Expression.Variable(srcType, "ns");
+ var tmpVar = Expression.Variable(destType, "nd");
+
+ return Expression.Block(
+ new[] { srcVar, tmpVar },
+ Expression.Assign(srcVar, srcValueExpr),
+ Expression.IfThen(
+ Expression.ReferenceNotEqual(srcVar, Expression.Constant(null, srcType)),
+ Expression.Block(
+ Expression.Assign(tmpVar, Expression.New(destType)),
+ Expression.Invoke(castDel, srcVar, tmpVar),
+ Expression.Assign(destAccess, tmpVar))));
+ }
+
+ private static Expression BuildCrossTypeCollectionCoerce(
+ Expression src,
+ Type destType,
+ Type srcElem,
+ Type dstElem,
+ Dictionary<(Type, Type), Delegate> mappings)
+ {
+ var delConst = Expression.Constant(mappings);
+ var keyConst = Expression.Constant((srcElem, dstElem));
+ var getDel = Expression.Call(
+ delConst,
+ _dictGetItemMethod,
+ keyConst);
+ var castDel = Expression.Convert(getDel, typeof(Action<,>).MakeGenericType(srcElem, dstElem));
+
+ var xParam = Expression.Parameter(srcElem, "x");
+ var tmpVar = Expression.Variable(dstElem, "tmp");
+ var selectorBody = Expression.Block(
+ new[] { tmpVar },
+ Expression.Assign(tmpVar, Expression.New(dstElem)),
+ Expression.Invoke(castDel, xParam, tmpVar),
+ tmpVar);
+ var selector = Expression.Lambda(selectorBody, xParam);
+
+ var iEnumSrc = typeof(IEnumerable<>).MakeGenericType(srcElem);
+ var srcCast = src.Type == iEnumSrc ? src : Expression.Convert(src, iEnumSrc);
+
+ var selectMethod = typeof(Enumerable)
+ .GetMethods()
+ .First(m => m.Name == nameof(Enumerable.Select) && m.GetParameters().Length == 2)
+ .MakeGenericMethod(srcElem, dstElem);
+
+ Expression filled;
+ if (destType.IsArray)
+ {
+ var toArray = typeof(Enumerable).GetMethod(nameof(Enumerable.ToArray))!.MakeGenericMethod(dstElem);
+ filled = Expression.Call(toArray, Expression.Call(selectMethod, srcCast, selector));
+ }
+ else
+ {
+ var toList = typeof(Enumerable).GetMethod(nameof(Enumerable.ToList))!.MakeGenericMethod(dstElem);
+ filled = Expression.Call(toList, Expression.Call(selectMethod, srcCast, selector));
+ }
+
+ Expression emptyExpr = destType.IsArray
+ ? Expression.NewArrayBounds(dstElem, Expression.Constant(0))
+ : (Expression)Expression.New(typeof(List<>).MakeGenericType(dstElem));
+
+ if (src.Type.IsValueType) return filled;
+ return Expression.Condition(
+ Expression.ReferenceEqual(src, Expression.Constant(null, src.Type)),
+ emptyExpr,
+ filled);
+ }
+
+ private static Expression InlineLambda(LambdaExpression lambda, Expression arg)
+ => new ParameterReplacer(lambda.Parameters[0], arg).Visit(lambda.Body);
+
+ private static Expression? SourceProp(ParameterExpression src, string name)
+ {
+ var sp = src.Type.GetProperty(name, BindingFlags.Public | BindingFlags.Instance);
+ return sp?.CanRead == true ? Expression.Property(src, sp) : null;
+ }
+
+ private static MemberExpression? SubstitutePath(LambdaExpression pathExpr, ParameterExpression dst)
+ {
+ var chain = new List();
+ var node = pathExpr.Body;
+ while (node is MemberExpression me && me.Member is PropertyInfo pi)
+ {
+ chain.Insert(0, pi);
+ node = me.Expression!;
+ }
+ if (chain.Count == 0 || node is not ParameterExpression) return null;
+
+ Expression result = dst;
+ foreach (var pi in chain)
+ result = Expression.Property(result, pi);
+ return result as MemberExpression;
+ }
+
+ private static Expression? Coerce(Expression? expr, Type target)
+ {
+ if (expr == null) return null;
+ if (expr.Type == target) return expr;
+
+ // T → Nullable
+ var underlyingTarget = Nullable.GetUnderlyingType(target);
+ if (underlyingTarget == expr.Type)
+ return Expression.Convert(expr, target);
+
+ return CoerceNullableToValue(expr, target)
+ ?? CoerceCollections(expr, target)
+ ?? CoerceUpcast(expr, target)
+ ?? CoerceNumericOrEnum(expr, target)
+ ?? CoerceViaConvertOperator(expr, target);
+ }
+
+ private static Expression? CoerceNullableToValue(Expression expr, Type target)
+ {
+ var underlyingSource = Nullable.GetUnderlyingType(expr.Type);
+ if (underlyingSource != target) return null;
+ return Expression.Condition(
+ Expression.Property(expr, nameof(Nullable.HasValue)),
+ Expression.Property(expr, nameof(Nullable.Value)),
+ Expression.Default(target));
+ }
+
+ private static Expression? CoerceCollections(Expression expr, Type target)
+ {
+ var srcElem = CollectionElementType(expr.Type);
+ var dstElem = CollectionElementType(target);
+ if (srcElem == null || dstElem == null || srcElem != dstElem) return null;
+ return BuildCollectionCoerce(expr, target, dstElem);
+ }
+
+ private static Expression? CoerceUpcast(Expression expr, Type target)
+ => target.IsAssignableFrom(expr.Type) ? expr : null;
+
+ private static Expression? CoerceNumericOrEnum(Expression expr, Type target)
+ {
+ if (!IsNumericOrEnum(expr.Type) || !IsNumericOrEnum(target)) return null;
+ try { return Expression.Convert(expr, target); } catch { return null; }
+ }
+
+ private static Expression? CoerceViaConvertOperator(Expression expr, Type target)
+ {
+ try { var c = Expression.Convert(expr, target); return c.Method != null ? c : null; }
+ catch { return null; }
+ }
+
+ private static Expression BuildCollectionCoerce(Expression src, Type target, Type elem)
+ {
+ var iEnum = typeof(IEnumerable<>).MakeGenericType(elem);
+ var srcCast = src.Type == iEnum ? src : Expression.Convert(src, iEnum);
+
+ Expression filled, empty;
+ if (target.IsArray)
+ {
+ var toArray = typeof(Enumerable).GetMethod(nameof(Enumerable.ToArray))!.MakeGenericMethod(elem);
+ filled = Expression.Call(toArray, srcCast);
+ empty = Expression.NewArrayBounds(elem, Expression.Constant(0));
+ }
+ else
+ {
+ var listType = typeof(List<>).MakeGenericType(elem);
+ var toList = typeof(Enumerable).GetMethod(nameof(Enumerable.ToList))!.MakeGenericMethod(elem);
+ filled = Expression.Call(toList, srcCast);
+ empty = Expression.New(listType);
+ }
+
+ if (src.Type.IsValueType) return filled;
+ return Expression.Condition(
+ Expression.ReferenceEqual(src, Expression.Constant(null, src.Type)),
+ empty, filled);
+ }
+
+ private static Type? CollectionElementType(Type t)
+ {
+ if (t == typeof(string)) return null;
+ if (t.IsArray && t.GetArrayRank() == 1) return t.GetElementType();
+ if (t.IsGenericType)
+ {
+ var def = t.GetGenericTypeDefinition();
+ if (def == typeof(List<>) || def == typeof(IList<>) || def == typeof(ICollection<>)
+ || def == typeof(IEnumerable<>) || def == typeof(IReadOnlyList<>)
+ || def == typeof(IReadOnlyCollection<>))
+ return t.GetGenericArguments()[0];
+ }
+ foreach (var iface in t.GetInterfaces())
+ if (iface.IsGenericType && iface.GetGenericTypeDefinition() == typeof(IEnumerable<>))
+ return iface.GetGenericArguments()[0];
+ return null;
+ }
+
+ private static bool IsNumericOrEnum(Type t)
+ {
+ t = Nullable.GetUnderlyingType(t) ?? t;
+ return t.IsEnum || t == typeof(byte) || t == typeof(sbyte)
+ || t == typeof(short) || t == typeof(ushort)
+ || t == typeof(int) || t == typeof(uint)
+ || t == typeof(long) || t == typeof(ulong)
+ || t == typeof(float) || t == typeof(double)
+ || t == typeof(decimal);
+ }
+}
diff --git a/src/Core/Grand.Mapping/Internal/MappingConfiguration.cs b/src/Core/Grand.Mapping/Internal/MappingConfiguration.cs
new file mode 100644
index 000000000..76ee1a0d3
--- /dev/null
+++ b/src/Core/Grand.Mapping/Internal/MappingConfiguration.cs
@@ -0,0 +1,14 @@
+namespace Grand.Mapping.Internal;
+
+internal sealed class MappingConfiguration : IMappingConfiguration
+{
+ private readonly MappingExpressionImpl _expression;
+
+ public MappingConfiguration(MappingExpressionImpl expression)
+ => _expression = expression;
+
+ public (Type Source, Type Dest) GetTypes() => (typeof(TSource), typeof(TDest));
+
+ public Delegate CompileDelegate(HashSet<(Type, Type)> registeredTypes, Dictionary<(Type, Type), Delegate> mappings)
+ => MappingCompiler.Compile(_expression.MemberConfigs, registeredTypes, mappings);
+}
diff --git a/src/Core/Grand.Mapping/Internal/MappingExpressionImpl.cs b/src/Core/Grand.Mapping/Internal/MappingExpressionImpl.cs
new file mode 100644
index 000000000..0a6457526
--- /dev/null
+++ b/src/Core/Grand.Mapping/Internal/MappingExpressionImpl.cs
@@ -0,0 +1,53 @@
+using System.Linq.Expressions;
+
+namespace Grand.Mapping.Internal;
+
+internal sealed class MappingExpressionImpl : IMappingExpression
+{
+ internal readonly List MemberConfigs = new();
+
+ public IMappingExpression ForMember(
+ Expression> destMember,
+ Action> opts)
+ {
+ var body = destMember.Body;
+ // Unwrap UnaryExpression conversion if present (e.g., boxing)
+ if (body is UnaryExpression unary) body = unary.Operand;
+ var config = new MemberConfig {
+ MemberName = ((MemberExpression)body).Member.Name,
+ IsPath = false
+ };
+ opts(new MemberConfigExpressionImpl(config));
+ MemberConfigs.Add(config);
+ return this;
+ }
+
+ public IMappingExpression ForPath(
+ Expression> destPath,
+ Action> opts)
+ {
+ var config = new MemberConfig {
+ DestinationPathExpression = destPath,
+ IsPath = true
+ };
+ opts(new MemberConfigExpressionImpl(config));
+ MemberConfigs.Add(config);
+ return this;
+ }
+}
+
+internal sealed class MemberConfigExpressionImpl
+ : IMemberConfigurationExpression
+{
+ private readonly MemberConfig _config;
+
+ public MemberConfigExpressionImpl(MemberConfig config) => _config = config;
+
+ public void Ignore() => _config.IsIgnored = true;
+
+ public void MapFrom(Expression> mapExpression)
+ => _config.MapFromExpression = mapExpression;
+
+ public void Condition(Expression> condition)
+ => _config.ConditionExpression = condition;
+}
diff --git a/src/Core/Grand.Mapping/Internal/MemberConfig.cs b/src/Core/Grand.Mapping/Internal/MemberConfig.cs
new file mode 100644
index 000000000..71b12113e
--- /dev/null
+++ b/src/Core/Grand.Mapping/Internal/MemberConfig.cs
@@ -0,0 +1,15 @@
+#nullable enable
+
+using System.Linq.Expressions;
+
+namespace Grand.Mapping.Internal;
+
+internal sealed class MemberConfig
+{
+ public bool IsIgnored { get; set; }
+ public LambdaExpression? MapFromExpression { get; set; }
+ public LambdaExpression? ConditionExpression { get; set; }
+ public string MemberName { get; set; } = "";
+ public LambdaExpression? DestinationPathExpression { get; set; }
+ public bool IsPath { get; set; }
+}
diff --git a/src/Core/Grand.Mapping/Internal/ParameterReplacer.cs b/src/Core/Grand.Mapping/Internal/ParameterReplacer.cs
new file mode 100644
index 000000000..098177665
--- /dev/null
+++ b/src/Core/Grand.Mapping/Internal/ParameterReplacer.cs
@@ -0,0 +1,22 @@
+using System.Linq.Expressions;
+
+namespace Grand.Mapping.Internal;
+
+///
+/// Substitutes a lambda parameter with a concrete expression, enabling body inlining
+/// instead of generating a delegate-call wrapper via Expression.Invoke.
+///
+internal sealed class ParameterReplacer : ExpressionVisitor
+{
+ private readonly ParameterExpression _parameter;
+ private readonly Expression _replacement;
+
+ internal ParameterReplacer(ParameterExpression parameter, Expression replacement)
+ {
+ _parameter = parameter;
+ _replacement = replacement;
+ }
+
+ protected override Expression VisitParameter(ParameterExpression node)
+ => node == _parameter ? _replacement : base.VisitParameter(node);
+}
diff --git a/src/Core/Grand.Mapping/MapperConfiguration.cs b/src/Core/Grand.Mapping/MapperConfiguration.cs
new file mode 100644
index 000000000..20fa3c96c
--- /dev/null
+++ b/src/Core/Grand.Mapping/MapperConfiguration.cs
@@ -0,0 +1,30 @@
+using Grand.Mapping.Internal;
+
+namespace Grand.Mapping;
+
+public sealed class MapperConfiguration
+{
+ private readonly Dictionary<(Type, Type), Delegate> _mappings = new();
+
+ public MapperConfiguration(Action configure)
+ {
+ var expr = new MapperConfigurationExpressionImpl();
+ configure(expr);
+ var configs = expr.GetConfigurations().ToList();
+
+ // First pass: register all type-pair keys so nested mappings can detect
+ // forward/cross references during compilation.
+ var registeredTypes = new HashSet<(Type, Type)>(configs.Count);
+ foreach (var config in configs)
+ registeredTypes.Add(config.GetTypes());
+
+ // Second pass: compile all delegates (all keys are now registered).
+ foreach (var config in configs)
+ {
+ var key = config.GetTypes();
+ _mappings[key] = config.CompileDelegate(registeredTypes, _mappings);
+ }
+ }
+
+ public IMapper CreateMapper() => new GrandMapper(_mappings);
+}
diff --git a/src/Core/Grand.Mapping/Profile.cs b/src/Core/Grand.Mapping/Profile.cs
new file mode 100644
index 000000000..0760becdc
--- /dev/null
+++ b/src/Core/Grand.Mapping/Profile.cs
@@ -0,0 +1,17 @@
+using Grand.Mapping.Internal;
+
+namespace Grand.Mapping;
+
+public abstract class Profile
+{
+ private readonly List _configurations = new();
+
+ protected IMappingExpression CreateMap() where TDest : new()
+ {
+ var expr = new MappingExpressionImpl();
+ _configurations.Add(new MappingConfiguration(expr));
+ return expr;
+ }
+
+ internal IEnumerable GetConfigurations() => _configurations;
+}
diff --git a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/AddressProfile.cs b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/AddressProfile.cs
index 47daeddd7..47df0ac81 100644
--- a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/AddressProfile.cs
+++ b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/AddressProfile.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Module.Api.DTOs.Customers;
using Grand.Domain.Common;
using Grand.Infrastructure.Mapper;
diff --git a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/BrandProfile.cs b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/BrandProfile.cs
index 7f97ae451..43618f375 100644
--- a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/BrandProfile.cs
+++ b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/BrandProfile.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Module.Api.DTOs.Catalog;
using Grand.Domain.Catalog;
using Grand.Infrastructure.Mapper;
diff --git a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/CategoryProfile.cs b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/CategoryProfile.cs
index 81134dc3f..bc346eeec 100644
--- a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/CategoryProfile.cs
+++ b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/CategoryProfile.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Module.Api.DTOs.Catalog;
using Grand.Domain.Catalog;
using Grand.Infrastructure.Mapper;
diff --git a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/CollectionProfile.cs b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/CollectionProfile.cs
index 52d6c3e9e..73f5ac508 100644
--- a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/CollectionProfile.cs
+++ b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/CollectionProfile.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Module.Api.DTOs.Catalog;
using Grand.Domain.Catalog;
using Grand.Infrastructure.Mapper;
diff --git a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/CustomerGroupProfile.cs b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/CustomerGroupProfile.cs
index d00fc1ba8..b5bc43bf0 100644
--- a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/CustomerGroupProfile.cs
+++ b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/CustomerGroupProfile.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Module.Api.DTOs.Customers;
using Grand.Domain.Customers;
using Grand.Infrastructure.Mapper;
diff --git a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/CustomerProfile.cs b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/CustomerProfile.cs
index f55ded707..4bad0aef5 100644
--- a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/CustomerProfile.cs
+++ b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/CustomerProfile.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Module.Api.DTOs.Customers;
using Grand.Domain.Common;
using Grand.Domain.Customers;
diff --git a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/PictureProfile.cs b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/PictureProfile.cs
index 6a2d98c25..4715e3106 100644
--- a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/PictureProfile.cs
+++ b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/PictureProfile.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Module.Api.DTOs.Common;
using Grand.Domain.Media;
using Grand.Infrastructure.Mapper;
diff --git a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/ProductAttributeMappingProfile.cs b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/ProductAttributeMappingProfile.cs
index 155d10a92..ddfda7512 100644
--- a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/ProductAttributeMappingProfile.cs
+++ b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/ProductAttributeMappingProfile.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Module.Api.DTOs.Catalog;
using Grand.Domain.Catalog;
using Grand.Infrastructure.Mapper;
diff --git a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/ProductAttributeProfile.cs b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/ProductAttributeProfile.cs
index 972fb270a..40bbdee3a 100644
--- a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/ProductAttributeProfile.cs
+++ b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/ProductAttributeProfile.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Module.Api.DTOs.Catalog;
using Grand.Domain.Catalog;
using Grand.Infrastructure.Mapper;
diff --git a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/ProductProfile.cs b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/ProductProfile.cs
index 3a108eb0b..bd4da4b88 100644
--- a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/ProductProfile.cs
+++ b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/ProductProfile.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Module.Api.DTOs.Catalog;
using Grand.Domain.Catalog;
using Grand.Infrastructure.Mapper;
diff --git a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/SpecificationAttributeProfile.cs b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/SpecificationAttributeProfile.cs
index a667583a1..a9a023292 100644
--- a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/SpecificationAttributeProfile.cs
+++ b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/SpecificationAttributeProfile.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Module.Api.DTOs.Catalog;
using Grand.Domain.Catalog;
using Grand.Infrastructure.Mapper;
diff --git a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/TierPriceProfile.cs b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/TierPriceProfile.cs
index 97b96fd01..39df1dd7c 100644
--- a/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/TierPriceProfile.cs
+++ b/src/Modules/Grand.Module.Api/Infrastructure/Mapper/Profiles/TierPriceProfile.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Module.Api.DTOs.Catalog;
using Grand.Domain.Catalog;
using Grand.Infrastructure.Mapper;
diff --git a/src/Plugins/Shipping.ShippingPoint/MapperConfiguration.cs b/src/Plugins/Shipping.ShippingPoint/MapperConfiguration.cs
index bb454edf8..deb429271 100644
--- a/src/Plugins/Shipping.ShippingPoint/MapperConfiguration.cs
+++ b/src/Plugins/Shipping.ShippingPoint/MapperConfiguration.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Infrastructure.Mapper;
using Shipping.ShippingPoint.Domain;
using Shipping.ShippingPoint.Models;
diff --git a/src/Plugins/Widgets.Slider/Infrastructure/Mapper/SliderMapperConfiguration.cs b/src/Plugins/Widgets.Slider/Infrastructure/Mapper/SliderMapperConfiguration.cs
index 26e914544..f723927da 100644
--- a/src/Plugins/Widgets.Slider/Infrastructure/Mapper/SliderMapperConfiguration.cs
+++ b/src/Plugins/Widgets.Slider/Infrastructure/Mapper/SliderMapperConfiguration.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Infrastructure.Mapper;
using Widgets.Slider.Domain;
using Widgets.Slider.Models;
diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/BrandImportDataObjectTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/BrandImportDataObjectTests.cs
index bf19ad0ca..ce73aed85 100644
--- a/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/BrandImportDataObjectTests.cs
+++ b/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/BrandImportDataObjectTests.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Business.Catalog.Services.Brands;
using Grand.Business.Catalog.Services.ExportImport;
using Grand.Business.Common.Services.Seo;
@@ -180,7 +180,7 @@ private void InitAutoMapper()
//create AutoMapper configuration
var config = new MapperConfiguration(cfg =>
{
- foreach (var instance in instances) cfg.AddProfile(instance.GetType());
+ foreach (var instance in instances) cfg.AddProfile((Grand.Mapping.Profile)instance);
});
//register automapper
diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/CategoryImportDataObjectTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/CategoryImportDataObjectTests.cs
index 7efa3521c..e83a9ac9a 100644
--- a/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/CategoryImportDataObjectTests.cs
+++ b/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/CategoryImportDataObjectTests.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Business.Catalog.Services.Categories;
using Grand.Business.Catalog.Services.ExportImport;
using Grand.Business.Common.Services.Security;
@@ -181,7 +181,7 @@ private void InitAutoMapper()
//create AutoMapper configuration
var config = new MapperConfiguration(cfg =>
{
- foreach (var instance in instances) cfg.AddProfile(instance.GetType());
+ foreach (var instance in instances) cfg.AddProfile((Grand.Mapping.Profile)instance);
});
//register automapper
diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/CollectionImportDataObjectTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/CollectionImportDataObjectTests.cs
index f387c9d7f..17c0d396e 100644
--- a/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/CollectionImportDataObjectTests.cs
+++ b/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/CollectionImportDataObjectTests.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Business.Catalog.Services.Collections;
using Grand.Business.Catalog.Services.ExportImport;
using Grand.Business.Common.Services.Security;
@@ -185,7 +185,7 @@ private void InitAutoMapper()
//create AutoMapper configuration
var config = new MapperConfiguration(cfg =>
{
- foreach (var instance in instances) cfg.AddProfile(instance.GetType());
+ foreach (var instance in instances) cfg.AddProfile((Grand.Mapping.Profile)instance);
});
//register automapper
diff --git a/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/ProductImportDataObjectTests.cs b/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/ProductImportDataObjectTests.cs
index 0e4035aaa..469f91791 100644
--- a/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/ProductImportDataObjectTests.cs
+++ b/src/Tests/Grand.Business.Catalog.Tests/Services/ExportImport/ProductImportDataObjectTests.cs
@@ -1,4 +1,4 @@
-using AutoMapper;
+using Grand.Mapping;
using Grand.Business.Catalog.Services.ExportImport;
using Grand.Business.Catalog.Services.Products;
using Grand.Business.Common.Services.Security;
@@ -279,7 +279,7 @@ private void InitAutoMapper()
//create AutoMapper configuration
var config = new MapperConfiguration(cfg =>
{
- foreach (var instance in instances) cfg.AddProfile(instance.GetType());
+ foreach (var instance in instances) cfg.AddProfile((Grand.Mapping.Profile)instance);
});
//register automapper
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.BrandLayoutModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.BrandLayoutModel_ToDomain.verified.txt
new file mode 100644
index 000000000..fdfdf853c
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.BrandLayoutModel_ToDomain.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: BrandDefault,
+ ViewPath: BrandTemplate.Simple,
+ DisplayOrder: 2,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.BrandLayout_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.BrandLayout_ToModel.verified.txt
new file mode 100644
index 000000000..bd0615d43
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.BrandLayout_ToModel.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: BrandDefault,
+ ViewPath: BrandTemplate.Simple,
+ DisplayOrder: 2,
+ Id: bl-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.CategoryLayoutModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.CategoryLayoutModel_ToDomain.verified.txt
new file mode 100644
index 000000000..50f26305e
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.CategoryLayoutModel_ToDomain.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: DefaultLayout,
+ ViewPath: CategoryTemplate.Simple,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.CategoryLayout_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.CategoryLayout_ToModel.verified.txt
new file mode 100644
index 000000000..8eb41339d
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.CategoryLayout_ToModel.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: DefaultLayout,
+ ViewPath: CategoryTemplate.Simple,
+ DisplayOrder: 1,
+ Id: cl-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.CollectionLayoutModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.CollectionLayoutModel_ToDomain.verified.txt
new file mode 100644
index 000000000..587944822
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.CollectionLayoutModel_ToDomain.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: CollectionDefault,
+ ViewPath: CollectionTemplate.Simple,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.CollectionLayout_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.CollectionLayout_ToModel.verified.txt
new file mode 100644
index 000000000..f5c891988
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.CollectionLayout_ToModel.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: CollectionDefault,
+ ViewPath: CollectionTemplate.Simple,
+ DisplayOrder: 1,
+ Id: cll-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.PageLayoutModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.PageLayoutModel_ToDomain.verified.txt
new file mode 100644
index 000000000..248413138
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.PageLayoutModel_ToDomain.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: PageDefault,
+ ViewPath: PageTemplate.Default,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.PageLayout_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.PageLayout_ToModel.verified.txt
new file mode 100644
index 000000000..688b11d7a
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.PageLayout_ToModel.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: PageDefault,
+ ViewPath: PageTemplate.Default,
+ DisplayOrder: 1,
+ Id: pgl-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.ProductLayoutModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.ProductLayoutModel_ToDomain.verified.txt
new file mode 100644
index 000000000..31c806dac
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.ProductLayoutModel_ToDomain.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: SimpleProduct,
+ ViewPath: ProductTemplate.Simple,
+ DisplayOrder: 5,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.ProductLayout_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.ProductLayout_ToModel.verified.txt
new file mode 100644
index 000000000..abeb681e2
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.ProductLayout_ToModel.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: SimpleProduct,
+ ViewPath: ProductTemplate.Simple,
+ DisplayOrder: 5,
+ Id: pl-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.cs b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.cs
new file mode 100644
index 000000000..638a8bacd
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogLayoutMappingTests.cs
@@ -0,0 +1,108 @@
+using Grand.Mapping;
+using Grand.Domain.Catalog;
+using Grand.Domain.Pages;
+using Grand.Web.AdminShared.Mapper;
+using Grand.Web.AdminShared.Models.Layouts;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using VerifyMSTest;
+
+namespace Grand.Mapping.Tests.AdminShared;
+
+[TestClass]
+public class CatalogLayoutMappingTests : VerifyBase
+{
+ private IMapper _mapper;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ var config = new MapperConfiguration(cfg => {
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ });
+ _mapper = config.CreateMapper();
+ }
+
+ // ── CategoryLayout ────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task CategoryLayout_ToModel()
+ {
+ var source = new CategoryLayout { Id = "cl-001", Name = "DefaultLayout", ViewPath = "CategoryTemplate.Simple", DisplayOrder = 1 };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task CategoryLayoutModel_ToDomain()
+ {
+ var model = new CategoryLayoutModel { Name = "DefaultLayout", ViewPath = "CategoryTemplate.Simple", DisplayOrder = 1 };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── BrandLayout ───────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task BrandLayout_ToModel()
+ {
+ var source = new BrandLayout { Id = "bl-001", Name = "BrandDefault", ViewPath = "BrandTemplate.Simple", DisplayOrder = 2 };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task BrandLayoutModel_ToDomain()
+ {
+ var model = new BrandLayoutModel { Name = "BrandDefault", ViewPath = "BrandTemplate.Simple", DisplayOrder = 2 };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── CollectionLayout ──────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task CollectionLayout_ToModel()
+ {
+ var source = new CollectionLayout { Id = "cll-001", Name = "CollectionDefault", ViewPath = "CollectionTemplate.Simple", DisplayOrder = 1 };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task CollectionLayoutModel_ToDomain()
+ {
+ var model = new CollectionLayoutModel { Name = "CollectionDefault", ViewPath = "CollectionTemplate.Simple", DisplayOrder = 1 };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── ProductLayout ─────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task ProductLayout_ToModel()
+ {
+ var source = new ProductLayout { Id = "pl-001", Name = "SimpleProduct", ViewPath = "ProductTemplate.Simple", DisplayOrder = 5 };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task ProductLayoutModel_ToDomain()
+ {
+ var model = new ProductLayoutModel { Name = "SimpleProduct", ViewPath = "ProductTemplate.Simple", DisplayOrder = 5 };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── PageLayout ────────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task PageLayout_ToModel()
+ {
+ var source = new PageLayout { Id = "pgl-001", Name = "PageDefault", ViewPath = "PageTemplate.Default", DisplayOrder = 1 };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task PageLayoutModel_ToDomain()
+ {
+ var model = new PageLayoutModel { Name = "PageDefault", ViewPath = "PageTemplate.Default", DisplayOrder = 1 };
+ return Verify(_mapper.Map(model));
+ }
+}
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.BrandModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.BrandModel_ToDomain.verified.txt
new file mode 100644
index 000000000..c025f8ba9
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.BrandModel_ToDomain.verified.txt
@@ -0,0 +1,30 @@
+{
+ Name: Apple,
+ Description: Apple brand,
+ BottomDescription: Bottom,
+ BrandLayoutId: layout-001,
+ MetaKeywords: apple,
+ MetaDescription: Apple products,
+ MetaTitle: Apple,
+ PictureId: pic-001,
+ PageSize: 20,
+ AllowCustomersToSelectPageSize: false,
+ PageSizeOptions: 20, 40,
+ ShowOnHomePage: true,
+ IncludeInMenu: true,
+ Icon: fa fa-apple,
+ DefaultSort: -1,
+ ExternalId: ext-brand-001,
+ Published: true,
+ DisplayOrder: 1,
+ LimitedToGroups: true,
+ CustomerGroups: [
+ grp-001
+ ],
+ LimitedToStores: true,
+ Stores: [
+ store-001,
+ store-002
+ ],
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Brand_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Brand_ToModel.verified.txt
new file mode 100644
index 000000000..199e43c94
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Brand_ToModel.verified.txt
@@ -0,0 +1,22 @@
+{
+ Name: Apple,
+ Description: Apple brand,
+ BottomDescription: Bottom,
+ BrandLayoutId: layout-001,
+ MetaKeywords: apple,
+ MetaDescription: Apple products,
+ MetaTitle: Apple,
+ SeName: apple,
+ PictureId: pic-001,
+ PageSize: 20,
+ AllowCustomersToSelectPageSize: false,
+ PageSizeOptions: 20, 40,
+ ShowOnHomePage: true,
+ IncludeInMenu: true,
+ Icon: fa fa-apple,
+ DefaultSort: -1,
+ Published: true,
+ DisplayOrder: 1,
+ ExternalId: ext-brand-001,
+ Id: brand-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.CategoryModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.CategoryModel_ToDomain.verified.txt
new file mode 100644
index 000000000..0244f4308
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.CategoryModel_ToDomain.verified.txt
@@ -0,0 +1,36 @@
+{
+ Name: Electronics,
+ Description: Electronic products,
+ BottomDescription: More electronics,
+ CategoryLayoutId: layout-001,
+ MetaKeywords: electronics, gadgets,
+ MetaDescription: Shop electronics online,
+ MetaTitle: Electronics,
+ ParentCategoryId: parent-001,
+ PictureId: pic-001,
+ PageSize: 15,
+ AllowCustomersToSelectPageSize: true,
+ PageSizeOptions: 15, 30, 60,
+ ShowOnHomePage: true,
+ FeaturedProductsOnHomePage: false,
+ ShowOnSearchBox: true,
+ SearchBoxDisplayOrder: 2,
+ IncludeInMenu: true,
+ Published: true,
+ DisplayOrder: 5,
+ ExternalId: ext-001,
+ Flag: New,
+ FlagStyle: bg-success,
+ Icon: fa fa-laptop,
+ HideOnCatalog: false,
+ LimitedToGroups: true,
+ CustomerGroups: [
+ grp-001,
+ grp-002
+ ],
+ LimitedToStores: true,
+ Stores: [
+ store-001
+ ],
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Category_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Category_ToModel.verified.txt
new file mode 100644
index 000000000..9283c92e5
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Category_ToModel.verified.txt
@@ -0,0 +1,28 @@
+{
+ Name: Electronics,
+ Description: Electronic products,
+ BottomDescription: More electronics,
+ CategoryLayoutId: layout-001,
+ MetaKeywords: electronics, gadgets,
+ MetaDescription: Shop electronics online,
+ MetaTitle: Electronics,
+ SeName: electronics,
+ ParentCategoryId: parent-001,
+ PictureId: pic-001,
+ PageSize: 15,
+ AllowCustomersToSelectPageSize: true,
+ PageSizeOptions: 15, 30, 60,
+ ShowOnHomePage: true,
+ FeaturedProductsOnHomePage: false,
+ IncludeInMenu: true,
+ Published: true,
+ DisplayOrder: 5,
+ ExternalId: ext-001,
+ Flag: New,
+ FlagStyle: bg-success,
+ Icon: fa fa-laptop,
+ HideOnCatalog: false,
+ ShowOnSearchBox: true,
+ SearchBoxDisplayOrder: 2,
+ Id: cat-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.CollectionModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.CollectionModel_ToDomain.verified.txt
new file mode 100644
index 000000000..fd1136589
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.CollectionModel_ToDomain.verified.txt
@@ -0,0 +1,18 @@
+{
+ Name: Summer Sale,
+ Description: Summer collection,
+ CollectionLayoutId: layout-002,
+ PageSize: 5,
+ AllowCustomersToSelectPageSize: false,
+ ShowOnHomePage: false,
+ FeaturedProductsOnHomePage: false,
+ IncludeInMenu: false,
+ Published: true,
+ DisplayOrder: 3,
+ LimitedToGroups: false,
+ LimitedToStores: true,
+ Stores: [
+ store-001
+ ],
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Collection_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Collection_ToModel.verified.txt
new file mode 100644
index 000000000..12f4e8dcb
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Collection_ToModel.verified.txt
@@ -0,0 +1,22 @@
+{
+ Name: Summer Sale,
+ Description: Summer collection,
+ BottomDescription: End of summer,
+ CollectionLayoutId: layout-002,
+ MetaKeywords: summer, sale,
+ MetaDescription: Summer sales,
+ MetaTitle: Summer Collection,
+ SeName: summer-sale,
+ PictureId: pic-002,
+ PageSize: 10,
+ AllowCustomersToSelectPageSize: true,
+ PageSizeOptions: 10, 20,
+ ShowOnHomePage: false,
+ FeaturedProductsOnHomePage: false,
+ IncludeInMenu: false,
+ DefaultSort: -1,
+ Published: true,
+ DisplayOrder: 3,
+ ExternalId: ext-col-001,
+ Id: col-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.DeliveryDateModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.DeliveryDateModel_ToDomain.verified.txt
new file mode 100644
index 000000000..a44118b17
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.DeliveryDateModel_ToDomain.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: 2-3 Business Days,
+ DisplayOrder: 1,
+ ColorSquaresRgb: #00FF00,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.DeliveryDate_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.DeliveryDate_ToModel.verified.txt
new file mode 100644
index 000000000..4751e7f6c
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.DeliveryDate_ToModel.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: 2-3 Business Days,
+ DisplayOrder: 1,
+ ColorSquaresRgb: #00FF00,
+ Id: dd-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.DiscountModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.DiscountModel_ToDomain.verified.txt
new file mode 100644
index 000000000..230194450
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.DiscountModel_ToDomain.verified.txt
@@ -0,0 +1,19 @@
+{
+ Name: 10% Off,
+ DiscountTypeId: AssignedToOrderTotal,
+ UsePercentage: true,
+ DiscountPercentage: 10.0,
+ CalculateByPlugin: false,
+ MaximumDiscountAmount: 0.0,
+ RequiresCouponCode: true,
+ Reused: false,
+ IsCumulative: false,
+ MaximumDiscountedQuantity: 0,
+ IsEnabled: true,
+ LimitedToStores: true,
+ Stores: [
+ store-001,
+ store-002
+ ],
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Discount_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Discount_ToModel.verified.txt
new file mode 100644
index 000000000..26f0ecace
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Discount_ToModel.verified.txt
@@ -0,0 +1,19 @@
+{
+ Name: 10% Off,
+ DiscountTypeId: 1,
+ UsePercentage: true,
+ DiscountPercentage: 10.0,
+ CurrencyCode: USD,
+ CalculateByPlugin: false,
+ MaximumDiscountAmount: 100.0,
+ RequiresCouponCode: true,
+ Reused: false,
+ IsCumulative: false,
+ LimitationTimes: 100,
+ MaximumDiscountedQuantity: 5,
+ IsEnabled: true,
+ Stores: [
+ store-001
+ ],
+ Id: disc-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Discount_ToModel_NullableFieldsAreNull.verified.txt.bak b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Discount_ToModel_NullableFieldsAreNull.verified.txt.bak
new file mode 100644
index 000000000..cfe089dd0
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Discount_ToModel_NullableFieldsAreNull.verified.txt.bak
@@ -0,0 +1,11 @@
+{
+ Name: No Dates Discount,
+ DiscountTypeId: 1,
+ UsePercentage: false,
+ CalculateByPlugin: false,
+ RequiresCouponCode: false,
+ Reused: false,
+ IsCumulative: false,
+ IsEnabled: true,
+ Id: disc-002
+}
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Discount_ToModel_NullableFields_WithNull.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Discount_ToModel_NullableFields_WithNull.verified.txt
new file mode 100644
index 000000000..f3d3f8d29
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Discount_ToModel_NullableFields_WithNull.verified.txt
@@ -0,0 +1,11 @@
+{
+ Name: No Dates Discount,
+ DiscountTypeId: 1,
+ UsePercentage: false,
+ CalculateByPlugin: false,
+ RequiresCouponCode: false,
+ Reused: false,
+ IsCumulative: false,
+ IsEnabled: true,
+ Id: disc-003
+}
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Discount_ToModel_NullableFields_WithValues.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Discount_ToModel_NullableFields_WithValues.verified.txt
new file mode 100644
index 000000000..fc9aae9e7
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.Discount_ToModel_NullableFields_WithValues.verified.txt
@@ -0,0 +1,12 @@
+{
+ Name: Summer Sale,
+ DiscountTypeId: 1,
+ UsePercentage: false,
+ CalculateByPlugin: false,
+ MaximumDiscountAmount: 50.0,
+ RequiresCouponCode: false,
+ Reused: false,
+ IsCumulative: false,
+ IsEnabled: true,
+ Id: disc-002
+}
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.GiftVoucher_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.GiftVoucher_ToModel.verified.txt
new file mode 100644
index 000000000..86d4228db
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.GiftVoucher_ToModel.verified.txt
@@ -0,0 +1,13 @@
+{
+ Amount: 50.0,
+ CurrencyCode: USD,
+ IsGiftVoucherActivated: true,
+ Code: GIFT50,
+ RecipientName: John Doe,
+ RecipientEmail: john@example.com,
+ SenderName: Jane,
+ SenderEmail: jane@example.com,
+ Message: Happy Birthday!,
+ IsRecipientNotified: false,
+ Id: gv-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.cs b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.cs
new file mode 100644
index 000000000..f5c79642c
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogMappingTests.cs
@@ -0,0 +1,344 @@
+using Grand.Mapping;
+using Grand.Domain.Catalog;
+using Grand.Domain.Discounts;
+using Grand.Domain.Orders;
+using Grand.Domain.Shipping;
+using Grand.Web.AdminShared.Mapper;
+using Grand.Web.AdminShared.Models.Catalog;
+using Grand.Web.AdminShared.Models.Discounts;
+using Grand.Web.AdminShared.Models.Orders;
+using Grand.Web.AdminShared.Models.Shipping;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using VerifyMSTest;
+
+namespace Grand.Mapping.Tests.AdminShared;
+
+[TestClass]
+public class CatalogMappingTests : VerifyBase
+{
+ private IMapper _mapper;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ var config = new MapperConfiguration(cfg => {
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ });
+ _mapper = config.CreateMapper();
+ }
+
+ // ── Category ──────────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task Category_ToModel()
+ {
+ var source = new Category {
+ Id = "cat-001",
+ Name = "Electronics",
+ Description = "Electronic products",
+ BottomDescription = "More electronics",
+ CategoryLayoutId = "layout-001",
+ MetaKeywords = "electronics, gadgets",
+ MetaDescription = "Shop electronics online",
+ MetaTitle = "Electronics",
+ ParentCategoryId = "parent-001",
+ PictureId = "pic-001",
+ PageSize = 15,
+ AllowCustomersToSelectPageSize = true,
+ PageSizeOptions = "15, 30, 60",
+ ShowOnHomePage = true,
+ FeaturedProductsOnHomePage = false,
+ ShowOnSearchBox = true,
+ SearchBoxDisplayOrder = 2,
+ IncludeInMenu = true,
+ Published = true,
+ DisplayOrder = 5,
+ ExternalId = "ext-001",
+ Flag = "New",
+ FlagStyle = "bg-success",
+ Icon = "fa fa-laptop",
+ HideOnCatalog = false,
+ DefaultSort = 0,
+ SeName = "electronics"
+ };
+ var result = _mapper.Map(source);
+ return Verify(result);
+ }
+
+ [TestMethod]
+ public Task CategoryModel_ToDomain()
+ {
+ var model = new CategoryModel {
+ Id = "cat-001",
+ Name = "Electronics",
+ Description = "Electronic products",
+ BottomDescription = "More electronics",
+ CategoryLayoutId = "layout-001",
+ MetaKeywords = "electronics, gadgets",
+ MetaDescription = "Shop electronics online",
+ MetaTitle = "Electronics",
+ ParentCategoryId = "parent-001",
+ PictureId = "pic-001",
+ PageSize = 15,
+ AllowCustomersToSelectPageSize = true,
+ PageSizeOptions = "15, 30, 60",
+ ShowOnHomePage = true,
+ FeaturedProductsOnHomePage = false,
+ ShowOnSearchBox = true,
+ SearchBoxDisplayOrder = 2,
+ IncludeInMenu = true,
+ Published = true,
+ DisplayOrder = 5,
+ ExternalId = "ext-001",
+ Flag = "New",
+ FlagStyle = "bg-success",
+ Icon = "fa fa-laptop",
+ HideOnCatalog = false,
+ DefaultSort = 0,
+ CustomerGroups = ["grp-001", "grp-002"],
+ Stores = ["store-001"]
+ };
+ var result = _mapper.Map(model);
+ return Verify(result);
+ }
+
+ // ── Brand ─────────────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task Brand_ToModel()
+ {
+ var source = new Brand {
+ Id = "brand-001",
+ Name = "Apple",
+ Description = "Apple brand",
+ BottomDescription = "Bottom",
+ BrandLayoutId = "layout-001",
+ MetaKeywords = "apple",
+ MetaDescription = "Apple products",
+ MetaTitle = "Apple",
+ PictureId = "pic-001",
+ PageSize = 20,
+ AllowCustomersToSelectPageSize = false,
+ PageSizeOptions = "20, 40",
+ ShowOnHomePage = true,
+ IncludeInMenu = true,
+ Icon = "fa fa-apple",
+ DefaultSort = -1,
+ Published = true,
+ DisplayOrder = 1,
+ ExternalId = "ext-brand-001",
+ SeName = "apple"
+ };
+ var result = _mapper.Map(source);
+ return Verify(result);
+ }
+
+ [TestMethod]
+ public Task BrandModel_ToDomain()
+ {
+ var model = new BrandModel {
+ Id = "brand-001",
+ Name = "Apple",
+ Description = "Apple brand",
+ BottomDescription = "Bottom",
+ BrandLayoutId = "layout-001",
+ MetaKeywords = "apple",
+ MetaDescription = "Apple products",
+ MetaTitle = "Apple",
+ PictureId = "pic-001",
+ PageSize = 20,
+ AllowCustomersToSelectPageSize = false,
+ PageSizeOptions = "20, 40",
+ ShowOnHomePage = true,
+ IncludeInMenu = true,
+ Icon = "fa fa-apple",
+ DefaultSort = -1,
+ Published = true,
+ DisplayOrder = 1,
+ ExternalId = "ext-brand-001",
+ CustomerGroups = ["grp-001"],
+ Stores = ["store-001", "store-002"]
+ };
+ var result = _mapper.Map(model);
+ return Verify(result);
+ }
+
+ // ── Collection ────────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task Collection_ToModel()
+ {
+ var source = new Collection {
+ Id = "col-001",
+ Name = "Summer Sale",
+ Description = "Summer collection",
+ BottomDescription = "End of summer",
+ CollectionLayoutId = "layout-002",
+ MetaKeywords = "summer, sale",
+ MetaDescription = "Summer sales",
+ MetaTitle = "Summer Collection",
+ PictureId = "pic-002",
+ PageSize = 10,
+ AllowCustomersToSelectPageSize = true,
+ PageSizeOptions = "10, 20",
+ ShowOnHomePage = false,
+ IncludeInMenu = false,
+ Published = true,
+ DisplayOrder = 3,
+ ExternalId = "ext-col-001",
+ SeName = "summer-sale"
+ };
+ var result = _mapper.Map(source);
+ return Verify(result);
+ }
+
+ [TestMethod]
+ public Task CollectionModel_ToDomain()
+ {
+ var model = new CollectionModel {
+ Id = "col-001",
+ Name = "Summer Sale",
+ Description = "Summer collection",
+ CollectionLayoutId = "layout-002",
+ Published = true,
+ DisplayOrder = 3,
+ Stores = ["store-001"]
+ };
+ var result = _mapper.Map(model);
+ return Verify(result);
+ }
+
+ // ── DeliveryDate ──────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task DeliveryDate_ToModel()
+ {
+ var source = new DeliveryDate {
+ Id = "dd-001",
+ Name = "2-3 Business Days",
+ DisplayOrder = 1,
+ ColorSquaresRgb = "#00FF00"
+ };
+ var result = _mapper.Map(source);
+ return Verify(result);
+ }
+
+ [TestMethod]
+ public Task DeliveryDateModel_ToDomain()
+ {
+ var model = new DeliveryDateModel {
+ Name = "2-3 Business Days",
+ DisplayOrder = 1,
+ ColorSquaresRgb = "#00FF00"
+ };
+ var result = _mapper.Map(model);
+ return Verify(result);
+ }
+
+ // ── Discount ──────────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task Discount_ToModel()
+ {
+ var source = new Discount {
+ Id = "disc-001",
+ Name = "10% Off",
+ DiscountTypeId = (DiscountType)1,
+ UsePercentage = true,
+ DiscountPercentage = 10,
+ DiscountAmount = 0,
+ MaximumDiscountAmount = 100,
+ StartDateUtc = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
+ EndDateUtc = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc),
+ RequiresCouponCode = true,
+ IsEnabled = true,
+ LimitationTimes = 100,
+ MaximumDiscountedQuantity = 5,
+ CurrencyCode = "USD",
+ Stores = ["store-001"]
+ };
+ var result = _mapper.Map(source);
+ return Verify(result);
+ }
+
+ [TestMethod]
+ public Task Discount_ToModel_NullableFields_WithValues()
+ {
+ var source = new Discount {
+ Id = "disc-002",
+ Name = "Summer Sale",
+ DiscountTypeId = (DiscountType)1,
+ MaximumDiscountAmount = 50.0,
+ StartDateUtc = new DateTime(2024, 6, 1, 0, 0, 0, DateTimeKind.Utc),
+ EndDateUtc = new DateTime(2024, 8, 31, 23, 59, 59, DateTimeKind.Utc),
+ IsEnabled = true
+ };
+ var result = _mapper.Map(source);
+ return Verify(result);
+ }
+
+ [TestMethod]
+ public Task Discount_ToModel_NullableFields_WithNull()
+ {
+ // Regression: Nullable → T coercion threw InvalidOperationException
+ // when the source nullable was null (e.g. MaximumDiscountAmount, StartDateUtc).
+ var source = new Discount {
+ Id = "disc-003",
+ Name = "No Dates Discount",
+ DiscountTypeId = (DiscountType)1,
+ // MaximumDiscountAmount = null (double?)
+ // StartDateUtc = null (DateTime?)
+ // EndDateUtc = null (DateTime?)
+ IsEnabled = true
+ };
+ var result = _mapper.Map(source);
+ return Verify(result);
+ }
+
+ [TestMethod]
+ public Task DiscountModel_ToDomain()
+ {
+ var model = new DiscountModel {
+ Id = "disc-001",
+ Name = "10% Off",
+ DiscountTypeId = 1,
+ UsePercentage = true,
+ DiscountPercentage = 10,
+ DiscountAmount = 0,
+ RequiresCouponCode = true,
+ IsEnabled = true,
+ Stores = ["store-001", "store-002"]
+ };
+ var result = _mapper.Map(model);
+ return Verify(result);
+ }
+
+ // ── GiftVoucher ───────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task GiftVoucher_ToModel()
+ {
+ var source = new GiftVoucher {
+ Id = "gv-001",
+ GiftVoucherTypeId = 0,
+ Amount = 50.00,
+ IsGiftVoucherActivated = true,
+ Code = "GIFT50",
+ RecipientName = "John Doe",
+ RecipientEmail = "john@example.com",
+ SenderName = "Jane",
+ SenderEmail = "jane@example.com",
+ Message = "Happy Birthday!",
+ IsRecipientNotified = false,
+ CreatedOnUtc = new DateTime(2024, 6, 1, 10, 0, 0, DateTimeKind.Utc),
+ CurrencyCode = "USD"
+ };
+ var result = _mapper.Map(source);
+ return Verify(result);
+ }
+}
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.PredefinedAttributeValueModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.PredefinedAttributeValueModel_ToDomain.verified.txt
new file mode 100644
index 000000000..bff6bdc2d
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.PredefinedAttributeValueModel_ToDomain.verified.txt
@@ -0,0 +1,9 @@
+{
+ Name: Red,
+ PriceAdjustment: 5.0,
+ WeightAdjustment: 0.1,
+ Cost: 1.5,
+ IsPreSelected: true,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.PredefinedAttributeValue_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.PredefinedAttributeValue_ToModel.verified.txt
new file mode 100644
index 000000000..b1b257b32
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.PredefinedAttributeValue_ToModel.verified.txt
@@ -0,0 +1,11 @@
+{
+ Name: Red,
+ PriceAdjustment: 5.0,
+ PriceAdjustmentStr: 5.00,
+ WeightAdjustment: 0.1,
+ WeightAdjustmentStr: 0.10,
+ Cost: 1.5,
+ IsPreSelected: true,
+ DisplayOrder: 1,
+ Id: ppav-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.PredefinedAttributeValue_ToProductAttributeValue.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.PredefinedAttributeValue_ToProductAttributeValue.verified.txt
new file mode 100644
index 000000000..dd52bf8f8
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.PredefinedAttributeValue_ToProductAttributeValue.verified.txt
@@ -0,0 +1,8 @@
+{
+ Name: Blue,
+ PriceAdjustment: 3.0,
+ Cost: 1.0,
+ IsPreSelected: false,
+ DisplayOrder: 2,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductAttributeCombinationModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductAttributeCombinationModel_ToDomain.verified.txt
new file mode 100644
index 000000000..63c526d18
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductAttributeCombinationModel_ToDomain.verified.txt
@@ -0,0 +1,9 @@
+{
+ StockQuantity: 10,
+ AllowOutOfStockOrders: false,
+ Sku: SKU-RED-M,
+ Gtin: 1234567890,
+ OverriddenPrice: 95.0,
+ NotifyAdminForQuantityBelow: 2,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductAttributeCombination_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductAttributeCombination_ToModel.verified.txt
new file mode 100644
index 000000000..89b41ac76
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductAttributeCombination_ToModel.verified.txt
@@ -0,0 +1,10 @@
+{
+ Id: pac-001,
+ StockQuantity: 10,
+ AllowOutOfStockOrders: false,
+ Sku: SKU-RED-M,
+ Gtin: 1234567890,
+ OverriddenPrice: 95.0,
+ NotifyAdminForQuantityBelow: 2,
+ UseMultipleWarehouses: false
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductAttributeMappingModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductAttributeMappingModel_ToDomain.verified.txt
new file mode 100644
index 000000000..a8c8ebe5d
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductAttributeMappingModel_ToDomain.verified.txt
@@ -0,0 +1,10 @@
+{
+ ProductAttributeId: pa-001,
+ TextPrompt: Choose color,
+ IsRequired: true,
+ ShowOnCatalogPage: false,
+ Combination: false,
+ AttributeControlTypeId: DropdownList,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductAttributeMapping_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductAttributeMapping_ToModel.verified.txt
new file mode 100644
index 000000000..1a7d15d0e
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductAttributeMapping_ToModel.verified.txt
@@ -0,0 +1,13 @@
+{
+ ProductAttributeId: pa-001,
+ TextPrompt: Choose color,
+ IsRequired: true,
+ ShowOnCatalogPage: false,
+ AttributeControlTypeId: DropdownList,
+ DisplayOrder: 1,
+ Combination: false,
+ ShouldHaveValues: false,
+ ValidationRulesAllowed: false,
+ ConditionAllowed: false,
+ Id: pam-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductAttributeModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductAttributeModel_ToDomain.verified.txt
new file mode 100644
index 000000000..92ddee524
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductAttributeModel_ToDomain.verified.txt
@@ -0,0 +1,9 @@
+{
+ Name: Color,
+ Description: Product color,
+ LimitedToStores: true,
+ Stores: [
+ store-001
+ ],
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductAttribute_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductAttribute_ToModel.verified.txt
new file mode 100644
index 000000000..4be5efdaf
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductAttribute_ToModel.verified.txt
@@ -0,0 +1,5 @@
+{
+ Name: Color,
+ Description: Product color,
+ Id: pa-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductModel_ToDomain.verified.txt
new file mode 100644
index 000000000..c7924c187
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductModel_ToDomain.verified.txt
@@ -0,0 +1,51 @@
+{
+ VisibleIndividually: true,
+ Name: Laptop Pro,
+ ShortDescription: High-performance laptop,
+ FullDescription: Full description,
+ ShowOnHomePage: false,
+ BestSeller: false,
+ AllowCustomerReviews: false,
+ Sku: LAPTOP-001,
+ IsGiftVoucher: false,
+ RequireOtherProducts: false,
+ AutoAddRequiredProducts: false,
+ IsDownload: false,
+ UnlimitedDownloads: false,
+ HasSampleDownload: false,
+ HasUserAgreement: false,
+ IsRecurring: false,
+ IncBothDate: false,
+ IsShipEnabled: false,
+ IsFreeShipping: false,
+ ShipSeparately: false,
+ IsTaxExempt: false,
+ IsTele: false,
+ UseMultipleWarehouses: false,
+ StockQuantity: 50,
+ StockAvailability: false,
+ DisplayStockQuantity: false,
+ LowStock: false,
+ AllowOutOfStockSubscriptions: false,
+ NotReturnable: false,
+ DisableBuyButton: false,
+ DisableWishlistButton: false,
+ AvailableForPreOrder: false,
+ CallForPrice: false,
+ Price: 999.99,
+ OldPrice: 1199.99,
+ EnteredPrice: false,
+ BasepriceEnabled: false,
+ MarkAsNew: false,
+ Weight: 2.5,
+ Length: 35.0,
+ Width: 24.0,
+ Height: 2.0,
+ AuctionEnded: false,
+ DisplayOrder: 1,
+ Published: true,
+ LimitedToGroups: false,
+ SeName: laptop-pro,
+ LimitedToStores: false,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductModel_ToDomain_WithCalendarData.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductModel_ToDomain_WithCalendarData.verified.txt
new file mode 100644
index 000000000..a9cb2d40d
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductModel_ToDomain_WithCalendarData.verified.txt
@@ -0,0 +1,40 @@
+{
+ VisibleIndividually: false,
+ Name: Calendar Product,
+ ShowOnHomePage: false,
+ BestSeller: false,
+ AllowCustomerReviews: false,
+ Sku: CAL-001,
+ IsGiftVoucher: false,
+ RequireOtherProducts: false,
+ AutoAddRequiredProducts: false,
+ IsDownload: false,
+ UnlimitedDownloads: false,
+ HasSampleDownload: false,
+ HasUserAgreement: false,
+ IsRecurring: false,
+ IncBothDate: true,
+ IsShipEnabled: false,
+ IsFreeShipping: false,
+ ShipSeparately: false,
+ IsTaxExempt: false,
+ IsTele: false,
+ UseMultipleWarehouses: false,
+ StockAvailability: false,
+ DisplayStockQuantity: false,
+ LowStock: false,
+ AllowOutOfStockSubscriptions: false,
+ NotReturnable: false,
+ DisableBuyButton: false,
+ DisableWishlistButton: false,
+ AvailableForPreOrder: false,
+ CallForPrice: false,
+ EnteredPrice: false,
+ BasepriceEnabled: false,
+ MarkAsNew: false,
+ AuctionEnded: false,
+ Published: false,
+ LimitedToGroups: false,
+ LimitedToStores: false,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductReviewModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductReviewModel_ToDomain.verified.txt
new file mode 100644
index 000000000..8ddeffef3
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductReviewModel_ToDomain.verified.txt
@@ -0,0 +1,11 @@
+{
+ CustomerId: cust-001,
+ ProductId: prod-001,
+ IsApproved: true,
+ Title: Great product,
+ ReviewText: Really impressed!,
+ ReplyText: Thank you!,
+ ConfirmedPurchase: false,
+ Rating: 5,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductSpecificationModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductSpecificationModel_ToDomain.verified.txt
new file mode 100644
index 000000000..1d78cb2ed
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductSpecificationModel_ToDomain.verified.txt
@@ -0,0 +1,10 @@
+{
+ AttributeTypeId: 1,
+ SpecificationAttributeId: sa-001,
+ SpecificationAttributeOptionId: sao-001,
+ CustomValue: Custom,
+ AllowFiltering: true,
+ ShowOnProductPage: true,
+ DisplayOrder: 2,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductSpecification_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductSpecification_ToModel.verified.txt
new file mode 100644
index 000000000..7b38f931e
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.ProductSpecification_ToModel.verified.txt
@@ -0,0 +1,10 @@
+{
+ Id: psa-001,
+ SpecificationAttributeId: sa-001,
+ AttributeTypeId: 1,
+ SpecificationAttributeOptionId: sao-001,
+ CustomValue: Custom,
+ AllowFiltering: true,
+ ShowOnProductPage: true,
+ DisplayOrder: 2
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.Product_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.Product_ToModel.verified.txt
new file mode 100644
index 000000000..c57b50cc9
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.Product_ToModel.verified.txt
@@ -0,0 +1,76 @@
+{
+ Id: prod-001,
+ AuctionEnded: false,
+ VisibleIndividually: true,
+ ProductLayoutId: pl-001,
+ Name: Laptop Pro,
+ ShortDescription: High-performance laptop,
+ FullDescription: A full description of the laptop,
+ AdminComment: Internal note,
+ BrandId: brand-001,
+ VendorId: vendor-001,
+ ShowOnHomePage: true,
+ BestSeller: false,
+ MetaKeywords: laptop,
+ MetaDescription: Buy laptop,
+ MetaTitle: Laptop,
+ SeName: laptop-pro,
+ AllowCustomerReviews: true,
+ Sku: LAPTOP-001,
+ Gtin: 1234567890,
+ IsGiftVoucher: false,
+ RequireOtherProducts: false,
+ AutoAddRequiredProducts: false,
+ IsDownload: false,
+ UnlimitedDownloads: false,
+ HasSampleDownload: false,
+ HasUserAgreement: false,
+ IsRecurring: false,
+ CalendarModel: {
+ Interval: 1,
+ IncBothDate: false,
+ Quantity: 1,
+ Monday: false,
+ Tuesday: false,
+ Wednesday: false,
+ Thursday: false,
+ Friday: false,
+ Saturday: false,
+ Sunday: false
+ },
+ IsShipEnabled: false,
+ IsFreeShipping: false,
+ ShipSeparately: false,
+ IsTaxExempt: false,
+ IsTele: false,
+ ManageInventoryMethodId: 1,
+ UseMultipleWarehouses: false,
+ StockQuantity: 50,
+ StockAvailability: false,
+ DisplayStockQuantity: true,
+ AllowOutOfStockSubscriptions: false,
+ NotReturnable: false,
+ DisableBuyButton: false,
+ DisableWishlistButton: false,
+ AvailableForPreOrder: false,
+ CallForPrice: false,
+ Price: 999.99,
+ OldPrice: 1199.99,
+ ProductCost: 600.0,
+ EnteredPrice: false,
+ BasepriceEnabled: false,
+ MarkAsNew: false,
+ Weight: 2.5,
+ Length: 35.0,
+ Width: 24.0,
+ Height: 2.0,
+ DisplayOrder: 1,
+ Published: true,
+ AddPictureModel: {
+ IsDefault: false
+ },
+ CopyProductModel: {
+ CopyImages: false,
+ Published: false
+ }
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.Product_ToModel_WithCalendarData.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.Product_ToModel_WithCalendarData.verified.txt
new file mode 100644
index 000000000..7cd17a864
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.Product_ToModel_WithCalendarData.verified.txt
@@ -0,0 +1,55 @@
+{
+ Id: prod-002,
+ AuctionEnded: false,
+ VisibleIndividually: false,
+ Name: Calendar Product,
+ ShowOnHomePage: false,
+ BestSeller: false,
+ AllowCustomerReviews: false,
+ Sku: CAL-001,
+ IsGiftVoucher: false,
+ RequireOtherProducts: false,
+ AutoAddRequiredProducts: false,
+ IsDownload: false,
+ UnlimitedDownloads: false,
+ HasSampleDownload: false,
+ HasUserAgreement: false,
+ IsRecurring: false,
+ CalendarModel: {
+ Interval: 1,
+ IncBothDate: true,
+ Quantity: 1,
+ Monday: false,
+ Tuesday: false,
+ Wednesday: false,
+ Thursday: false,
+ Friday: false,
+ Saturday: false,
+ Sunday: false
+ },
+ IsShipEnabled: false,
+ IsFreeShipping: false,
+ ShipSeparately: false,
+ IsTaxExempt: false,
+ IsTele: false,
+ UseMultipleWarehouses: false,
+ StockAvailability: false,
+ DisplayStockQuantity: false,
+ AllowOutOfStockSubscriptions: false,
+ NotReturnable: false,
+ DisableBuyButton: false,
+ DisableWishlistButton: false,
+ AvailableForPreOrder: false,
+ CallForPrice: false,
+ EnteredPrice: false,
+ BasepriceEnabled: false,
+ MarkAsNew: false,
+ Published: false,
+ AddPictureModel: {
+ IsDefault: false
+ },
+ CopyProductModel: {
+ CopyImages: false,
+ Published: false
+ }
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.SpecificationAttributeModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.SpecificationAttributeModel_ToDomain.verified.txt
new file mode 100644
index 000000000..25686e374
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.SpecificationAttributeModel_ToDomain.verified.txt
@@ -0,0 +1,9 @@
+{
+ Name: Screen Size,
+ DisplayOrder: 1,
+ LimitedToStores: true,
+ Stores: [
+ store-001
+ ],
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.SpecificationAttributeOptionModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.SpecificationAttributeOptionModel_ToDomain.verified.txt
new file mode 100644
index 000000000..5bed76332
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.SpecificationAttributeOptionModel_ToDomain.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: 15 inch,
+ ColorSquaresRgb: #FFFFFF,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.SpecificationAttributeOption_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.SpecificationAttributeOption_ToModel.verified.txt
new file mode 100644
index 000000000..46d52833e
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.SpecificationAttributeOption_ToModel.verified.txt
@@ -0,0 +1,8 @@
+{
+ Name: 15 inch,
+ SeName: 15-inch,
+ ColorSquaresRgb: #FFFFFF,
+ EnableColorSquaresRgb: false,
+ DisplayOrder: 1,
+ Id: sao-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.SpecificationAttribute_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.SpecificationAttribute_ToModel.verified.txt
new file mode 100644
index 000000000..8abbb8e88
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.SpecificationAttribute_ToModel.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: Screen Size,
+ SeName: screen-size,
+ DisplayOrder: 1,
+ Id: sattr-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.TierPriceModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.TierPriceModel_ToDomain.verified.txt
new file mode 100644
index 000000000..123f9ea59
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.TierPriceModel_ToDomain.verified.txt
@@ -0,0 +1,8 @@
+{
+ StoreId: store-001,
+ CustomerGroupId: grp-001,
+ CurrencyCode: USD,
+ Quantity: 10,
+ Price: 89.99,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.TierPrice_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.TierPrice_ToModel.verified.txt
new file mode 100644
index 000000000..4cfdfb247
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.TierPrice_ToModel.verified.txt
@@ -0,0 +1,8 @@
+{
+ CustomerGroupId: grp-001,
+ StoreId: store-001,
+ CurrencyCode: USD,
+ Quantity: 10,
+ Price: 89.99,
+ Id: tp-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.cs b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.cs
new file mode 100644
index 000000000..74aa98c2d
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CatalogProductMappingTests.cs
@@ -0,0 +1,382 @@
+using Grand.Mapping;
+using Grand.Domain.Catalog;
+using Grand.Web.AdminShared.Mapper;
+using Grand.Web.AdminShared.Models.Catalog;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using VerifyMSTest;
+
+namespace Grand.Mapping.Tests.AdminShared;
+
+[TestClass]
+public class CatalogProductMappingTests : VerifyBase
+{
+ private IMapper _mapper;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ var config = new MapperConfiguration(cfg => {
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ });
+ _mapper = config.CreateMapper();
+ }
+
+ // ── Product ───────────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task Product_ToModel()
+ {
+ var source = new Product {
+ Id = "prod-001",
+ Name = "Laptop Pro",
+ ShortDescription = "High-performance laptop",
+ FullDescription = "A full description of the laptop",
+ AdminComment = "Internal note",
+ ProductLayoutId = "pl-001",
+ BrandId = "brand-001",
+ VendorId = "vendor-001",
+ ShowOnHomePage = true,
+ MetaKeywords = "laptop",
+ MetaDescription = "Buy laptop",
+ MetaTitle = "Laptop",
+ AllowCustomerReviews = true,
+ Published = true,
+ VisibleIndividually = true,
+ Price = 999.99,
+ OldPrice = 1199.99,
+ ProductCost = 600,
+ MarkAsNew = false,
+ Weight = 2.5,
+ Length = 35,
+ Width = 24,
+ Height = 2,
+ DisplayOrder = 1,
+ DisplayStockQuantity = true,
+ ManageInventoryMethodId = (ManageInventoryMethod)1,
+ StockQuantity = 50,
+ Sku = "LAPTOP-001",
+ Gtin = "1234567890",
+ SeName = "laptop-pro"
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task ProductModel_ToDomain()
+ {
+ var model = new ProductModel {
+ Id = "prod-001",
+ Name = "Laptop Pro",
+ ShortDescription = "High-performance laptop",
+ FullDescription = "Full description",
+ Published = true,
+ VisibleIndividually = true,
+ Price = 999.99,
+ OldPrice = 1199.99,
+ Sku = "LAPTOP-001",
+ Weight = 2.5,
+ Length = 35,
+ Width = 24,
+ Height = 2,
+ StockQuantity = 50,
+ DisplayOrder = 1,
+ SeName = "laptop-pro"
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ [TestMethod]
+ public Task Product_ToModel_WithCalendarData()
+ {
+ var source = new Product {
+ Id = "prod-002",
+ Name = "Calendar Product",
+ Sku = "CAL-001",
+ IncBothDate = true
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task ProductModel_ToDomain_WithCalendarData()
+ {
+ var model = new ProductModel {
+ Name = "Calendar Product",
+ Sku = "CAL-001",
+ CalendarModel = new ProductModel.GenerateCalendarModel {
+ IncBothDate = true
+ }
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── TierPrice ─────────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task TierPrice_ToModel()
+ {
+ var source = new TierPrice {
+ Id = "tp-001",
+ StoreId = "store-001",
+ CustomerGroupId = "grp-001",
+ Quantity = 10,
+ Price = 89.99,
+ StartDateTimeUtc = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
+ EndDateTimeUtc = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc),
+ CurrencyCode = "USD"
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task TierPriceModel_ToDomain()
+ {
+ var model = new ProductModel.TierPriceModel {
+ StoreId = "store-001",
+ CustomerGroupId = "grp-001",
+ Quantity = 10,
+ Price = 89.99,
+ CurrencyCode = "USD"
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── ProductSpecificationAttribute ─────────────────────────────────────────
+
+ [TestMethod]
+ public Task ProductSpecification_ToModel()
+ {
+ var source = new ProductSpecificationAttribute {
+ Id = "psa-001",
+ AttributeTypeId = (SpecificationAttributeType)1,
+ SpecificationAttributeId = "sa-001",
+ SpecificationAttributeOptionId = "sao-001",
+ CustomValue = "Custom",
+ AllowFiltering = true,
+ ShowOnProductPage = true,
+ DisplayOrder = 2
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task ProductSpecificationModel_ToDomain()
+ {
+ var model = new ProductModel.AddProductSpecificationAttributeModel {
+ AttributeTypeId = (SpecificationAttributeType)1,
+ SpecificationAttributeId = "sa-001",
+ SpecificationAttributeOptionId = "sao-001",
+ CustomValue = "Custom",
+ AllowFiltering = true,
+ ShowOnProductPage = true,
+ DisplayOrder = 2
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── ProductAttribute ──────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task ProductAttribute_ToModel()
+ {
+ var source = new ProductAttribute {
+ Id = "pa-001",
+ Name = "Color",
+ Description = "Product color"
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task ProductAttributeModel_ToDomain()
+ {
+ var model = new ProductAttributeModel {
+ Name = "Color",
+ Description = "Product color",
+ Stores = ["store-001"]
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── PredefinedProductAttributeValue ───────────────────────────────────────
+
+ [TestMethod]
+ public Task PredefinedAttributeValue_ToModel()
+ {
+ var source = new PredefinedProductAttributeValue {
+ Id = "ppav-001",
+ Name = "Red",
+ PriceAdjustment = 5.00,
+ WeightAdjustment = 0.1,
+ Cost = 1.5,
+ IsPreSelected = true,
+ DisplayOrder = 1
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task PredefinedAttributeValueModel_ToDomain()
+ {
+ var model = new PredefinedProductAttributeValueModel {
+ Name = "Red",
+ PriceAdjustment = 5.00,
+ WeightAdjustment = 0.1,
+ Cost = 1.5,
+ IsPreSelected = true,
+ DisplayOrder = 1
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── ProductAttributeMapping ───────────────────────────────────────────────
+
+ [TestMethod]
+ public Task ProductAttributeMapping_ToModel()
+ {
+ var source = new ProductAttributeMapping {
+ Id = "pam-001",
+ ProductAttributeId = "pa-001",
+ TextPrompt = "Choose color",
+ IsRequired = true,
+ AttributeControlTypeId = (AttributeControlType)1,
+ DisplayOrder = 1
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task ProductAttributeMappingModel_ToDomain()
+ {
+ var model = new ProductModel.ProductAttributeMappingModel {
+ ProductAttributeId = "pa-001",
+ TextPrompt = "Choose color",
+ IsRequired = true,
+ AttributeControlTypeId = (AttributeControlType)1,
+ DisplayOrder = 1
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── ProductAttributeCombination ───────────────────────────────────────────
+
+ [TestMethod]
+ public Task ProductAttributeCombination_ToModel()
+ {
+ var source = new ProductAttributeCombination {
+ Id = "pac-001",
+ Sku = "SKU-RED-M",
+ Gtin = "1234567890",
+ AllowOutOfStockOrders = false,
+ StockQuantity = 10,
+ OverriddenPrice = 95.00,
+ NotifyAdminForQuantityBelow = 2
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task ProductAttributeCombinationModel_ToDomain()
+ {
+ var model = new ProductAttributeCombinationModel {
+ Sku = "SKU-RED-M",
+ Gtin = "1234567890",
+ AllowOutOfStockOrders = false,
+ StockQuantity = 10,
+ OverriddenPrice = 95.00,
+ NotifyAdminForQuantityBelow = 2
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── PredefinedAttributeValue → ProductAttributeValue ─────────────────────
+
+ [TestMethod]
+ public Task PredefinedAttributeValue_ToProductAttributeValue()
+ {
+ var source = new PredefinedProductAttributeValue {
+ Id = "ppav-001",
+ Name = "Blue",
+ PriceAdjustment = 3.00,
+ WeightAdjustment = 0,
+ Cost = 1.0,
+ IsPreSelected = false,
+ DisplayOrder = 2
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ // ── ProductReview (model → domain) ────────────────────────────────────────
+
+ [TestMethod]
+ public Task ProductReviewModel_ToDomain()
+ {
+ var model = new ProductReviewModel {
+ CustomerId = "cust-001",
+ ProductId = "prod-001",
+ IsApproved = true,
+ Title = "Great product",
+ ReviewText = "Really impressed!",
+ ReplyText = "Thank you!",
+ Rating = 5
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── SpecificationAttribute ────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task SpecificationAttribute_ToModel()
+ {
+ var source = new SpecificationAttribute {
+ Id = "sattr-001",
+ Name = "Screen Size",
+ DisplayOrder = 1,
+ SeName = "screen-size"
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task SpecificationAttributeModel_ToDomain()
+ {
+ var model = new SpecificationAttributeModel {
+ Name = "Screen Size",
+ DisplayOrder = 1,
+ Stores = ["store-001"]
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ [TestMethod]
+ public Task SpecificationAttributeOption_ToModel()
+ {
+ var source = new SpecificationAttributeOption {
+ Id = "sao-001",
+ Name = "15 inch",
+ DisplayOrder = 1,
+ ColorSquaresRgb = "#FFFFFF",
+ SeName = "15-inch"
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task SpecificationAttributeOptionModel_ToDomain()
+ {
+ var model = new SpecificationAttributeOptionModel {
+ Name = "15 inch",
+ DisplayOrder = 1,
+ ColorSquaresRgb = "#FFFFFF"
+ };
+ return Verify(_mapper.Map(model));
+ }
+}
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.AddressAttributeModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.AddressAttributeModel_ToDomain.verified.txt
new file mode 100644
index 000000000..146d5dde0
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.AddressAttributeModel_ToDomain.verified.txt
@@ -0,0 +1,8 @@
+{
+ Name: Custom Field,
+ IsRequired: true,
+ AttributeControlTypeId: 1,
+ DisplayOrder: 1,
+ AttributeControlType: DropdownList,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.AddressAttributeValueModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.AddressAttributeValueModel_ToDomain.verified.txt
new file mode 100644
index 000000000..cbd60ef10
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.AddressAttributeValueModel_ToDomain.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: Option A,
+ IsPreSelected: true,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.AddressAttributeValue_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.AddressAttributeValue_ToModel.verified.txt
new file mode 100644
index 000000000..92d369196
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.AddressAttributeValue_ToModel.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: Option A,
+ IsPreSelected: true,
+ DisplayOrder: 1,
+ Id: aav-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.AddressAttribute_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.AddressAttribute_ToModel.verified.txt
new file mode 100644
index 000000000..23f3a0fe5
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.AddressAttribute_ToModel.verified.txt
@@ -0,0 +1,7 @@
+{
+ Name: Custom Field,
+ IsRequired: true,
+ AttributeControlTypeId: 1,
+ DisplayOrder: 1,
+ Id: aa-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.AddressModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.AddressModel_ToDomain.verified.txt
new file mode 100644
index 000000000..9aa6044f1
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.AddressModel_ToDomain.verified.txt
@@ -0,0 +1,14 @@
+{
+ FirstName: John,
+ LastName: Doe,
+ Email: john@example.com,
+ Company: ACME Corp,
+ CountryId: country-001,
+ StateProvinceId: state-001,
+ City: New York,
+ Address1: 123 Main St,
+ Address2: Apt 4B,
+ ZipPostalCode: 10001,
+ PhoneNumber: +1-555-1234,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Address_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Address_ToModel.verified.txt
new file mode 100644
index 000000000..db06a72f7
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Address_ToModel.verified.txt
@@ -0,0 +1,42 @@
+{
+ FirstName: John,
+ LastName: Doe,
+ Email: john@example.com,
+ Company: ACME Corp,
+ CountryId: country-001,
+ StateProvinceId: state-001,
+ City: New York,
+ Address1: 123 Main St,
+ Address2: Apt 4B,
+ ZipPostalCode: 10001,
+ PhoneNumber: +1-555-1234,
+ FaxNumber: +1-555-5678,
+ NameEnabled: false,
+ FirstNameEnabled: false,
+ FirstNameRequired: false,
+ LastNameEnabled: false,
+ LastNameRequired: false,
+ EmailEnabled: false,
+ EmailRequired: false,
+ CompanyEnabled: false,
+ CompanyRequired: false,
+ VatNumberEnabled: false,
+ VatNumberRequired: false,
+ CountryEnabled: false,
+ StateProvinceEnabled: false,
+ CityEnabled: false,
+ CityRequired: false,
+ StreetAddressEnabled: false,
+ StreetAddressRequired: false,
+ StreetAddress2Enabled: false,
+ StreetAddress2Required: false,
+ ZipPostalCodeEnabled: false,
+ ZipPostalCodeRequired: false,
+ PhoneEnabled: false,
+ PhoneRequired: false,
+ FaxEnabled: false,
+ FaxRequired: false,
+ NoteEnabled: false,
+ AddressTypeEnabled: false,
+ Id: addr-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.BankAccount_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.BankAccount_ToModel.verified.txt
new file mode 100644
index 000000000..08e60d0ec
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.BankAccount_ToModel.verified.txt
@@ -0,0 +1,7 @@
+{
+ BankCode: CHASE123,
+ BankName: Chase,
+ SwiftCode: CHASUS33,
+ AccountNumber: 123456789,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.CountryModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.CountryModel_ToDomain.verified.txt
new file mode 100644
index 000000000..3c24f30ce
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.CountryModel_ToDomain.verified.txt
@@ -0,0 +1,16 @@
+{
+ Name: United States,
+ AllowsBilling: true,
+ AllowsShipping: true,
+ TwoLetterIsoCode: US,
+ ThreeLetterIsoCode: USA,
+ NumericIsoCode: 840,
+ SubjectToVat: false,
+ Published: true,
+ DisplayOrder: 1,
+ LimitedToStores: true,
+ Stores: [
+ store-001
+ ],
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Country_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Country_ToModel.verified.txt
new file mode 100644
index 000000000..946805143
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Country_ToModel.verified.txt
@@ -0,0 +1,13 @@
+{
+ Name: United States,
+ AllowsBilling: true,
+ AllowsShipping: true,
+ TwoLetterIsoCode: US,
+ ThreeLetterIsoCode: USA,
+ NumericIsoCode: 840,
+ SubjectToVat: false,
+ Published: true,
+ DisplayOrder: 1,
+ NumberOfStates: 1,
+ Id: country-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.CurrencyModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.CurrencyModel_ToDomain.verified.txt
new file mode 100644
index 000000000..9f1edbc2d
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.CurrencyModel_ToDomain.verified.txt
@@ -0,0 +1,13 @@
+{
+ Name: US Dollar,
+ CurrencyCode: USD,
+ Rate: 1.0,
+ DisplayLocale: en-US,
+ Published: true,
+ DisplayOrder: 1,
+ LimitedToStores: true,
+ Stores: [
+ store-001
+ ],
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Currency_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Currency_ToModel.verified.txt
new file mode 100644
index 000000000..4f6efd9a7
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Currency_ToModel.verified.txt
@@ -0,0 +1,13 @@
+{
+ Name: US Dollar,
+ CurrencyCode: USD,
+ DisplayLocale: en-US,
+ Rate: 1.0,
+ CustomFormatting: ${0:N2},
+ NumberDecimal: 2,
+ Published: true,
+ DisplayOrder: 1,
+ IsPrimaryExchangeRateCurrency: false,
+ IsPrimaryStoreCurrency: false,
+ Id: cur-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.DocumentModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.DocumentModel_ToDomain.verified.txt
new file mode 100644
index 000000000..479b9b2b7
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.DocumentModel_ToDomain.verified.txt
@@ -0,0 +1,15 @@
+{
+ Number: INV-001,
+ Name: Invoice #001,
+ Published: true,
+ DocumentTypeId: dt-001,
+ LimitedToGroups: true,
+ CustomerGroups: [
+ grp-001
+ ],
+ LimitedToStores: true,
+ Stores: [
+ store-001
+ ],
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.DocumentTypeModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.DocumentTypeModel_ToDomain.verified.txt
new file mode 100644
index 000000000..ede2b9b71
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.DocumentTypeModel_ToDomain.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: Invoice,
+ Description: An invoice document,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.DocumentType_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.DocumentType_ToModel.verified.txt
new file mode 100644
index 000000000..6ec218b4f
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.DocumentType_ToModel.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: Invoice,
+ Description: An invoice document,
+ DisplayOrder: 1,
+ Id: dt-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Document_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Document_ToModel.verified.txt
new file mode 100644
index 000000000..7a0fbc849
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Document_ToModel.verified.txt
@@ -0,0 +1,12 @@
+{
+ Number: INV-001,
+ Name: Invoice #001,
+ Published: true,
+ DisplayOrder: 1,
+ CustomerId: cust-001,
+ ReferenceId: 10,
+ DocumentTypeId: dt-001,
+ CurrencyCode: USD,
+ TotalAmount: 500.0,
+ Id: doc-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.DomainHostModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.DomainHostModel_ToDomain.verified.txt
new file mode 100644
index 000000000..5a573bdc3
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.DomainHostModel_ToDomain.verified.txt
@@ -0,0 +1,5 @@
+{
+ Url: https://example.com,
+ Primary: true,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.DomainHost_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.DomainHost_ToModel.verified.txt
new file mode 100644
index 000000000..ecb9817b3
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.DomainHost_ToModel.verified.txt
@@ -0,0 +1,5 @@
+{
+ Url: https://example.com,
+ Primary: true,
+ Id: dh-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.LanguageModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.LanguageModel_ToDomain.verified.txt
new file mode 100644
index 000000000..bf3bf0e03
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.LanguageModel_ToDomain.verified.txt
@@ -0,0 +1,14 @@
+{
+ Name: English,
+ LanguageCulture: en-US,
+ UniqueSeoCode: en,
+ FlagImageFileName: us.png,
+ Rtl: false,
+ Published: true,
+ DisplayOrder: 1,
+ LimitedToStores: true,
+ Stores: [
+ store-001
+ ],
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Language_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Language_ToModel.verified.txt
new file mode 100644
index 000000000..73282184a
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Language_ToModel.verified.txt
@@ -0,0 +1,12 @@
+{
+ Name: English,
+ LanguageCulture: en-US,
+ UniqueSeoCode: en,
+ FlagImageFileName: us.png,
+ Rtl: false,
+ DefaultCurrencyId: cur-001,
+ Published: true,
+ DisplayOrder: 1,
+ Search: {},
+ Id: lang-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MeasureDimensionModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MeasureDimensionModel_ToDomain.verified.txt
new file mode 100644
index 000000000..b7484f650
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MeasureDimensionModel_ToDomain.verified.txt
@@ -0,0 +1,7 @@
+{
+ Name: Centimeter,
+ SystemKeyword: cm,
+ Ratio: 1.0,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MeasureDimension_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MeasureDimension_ToModel.verified.txt
new file mode 100644
index 000000000..cfbb9c902
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MeasureDimension_ToModel.verified.txt
@@ -0,0 +1,8 @@
+{
+ Name: Centimeter,
+ SystemKeyword: cm,
+ Ratio: 1.0,
+ DisplayOrder: 1,
+ IsPrimaryDimension: false,
+ Id: md-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MeasureUnitModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MeasureUnitModel_ToDomain.verified.txt
new file mode 100644
index 000000000..b0f4d5cde
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MeasureUnitModel_ToDomain.verified.txt
@@ -0,0 +1,5 @@
+{
+ Name: Piece,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MeasureUnit_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MeasureUnit_ToModel.verified.txt
new file mode 100644
index 000000000..83c9fe2ec
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MeasureUnit_ToModel.verified.txt
@@ -0,0 +1,5 @@
+{
+ Name: Piece,
+ DisplayOrder: 1,
+ Id: mu-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MeasureWeightModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MeasureWeightModel_ToDomain.verified.txt
new file mode 100644
index 000000000..5fe102c72
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MeasureWeightModel_ToDomain.verified.txt
@@ -0,0 +1,7 @@
+{
+ Name: Kilogram,
+ SystemKeyword: kg,
+ Ratio: 1.0,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MeasureWeight_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MeasureWeight_ToModel.verified.txt
new file mode 100644
index 000000000..f5e463e33
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MeasureWeight_ToModel.verified.txt
@@ -0,0 +1,8 @@
+{
+ Name: Kilogram,
+ SystemKeyword: kg,
+ Ratio: 1.0,
+ DisplayOrder: 1,
+ IsPrimaryWeight: false,
+ Id: mw-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MerchandiseReturnActionModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MerchandiseReturnActionModel_ToDomain.verified.txt
new file mode 100644
index 000000000..4a3b6777a
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MerchandiseReturnActionModel_ToDomain.verified.txt
@@ -0,0 +1,5 @@
+{
+ Name: Refund,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MerchandiseReturnAction_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MerchandiseReturnAction_ToModel.verified.txt
new file mode 100644
index 000000000..4c5f3a6d6
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MerchandiseReturnAction_ToModel.verified.txt
@@ -0,0 +1,5 @@
+{
+ Name: Refund,
+ DisplayOrder: 1,
+ Id: mra-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MerchandiseReturnReasonModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MerchandiseReturnReasonModel_ToDomain.verified.txt
new file mode 100644
index 000000000..5c671d38b
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MerchandiseReturnReasonModel_ToDomain.verified.txt
@@ -0,0 +1,5 @@
+{
+ Name: Defective,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MerchandiseReturnReason_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MerchandiseReturnReason_ToModel.verified.txt
new file mode 100644
index 000000000..4423baa4b
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.MerchandiseReturnReason_ToModel.verified.txt
@@ -0,0 +1,5 @@
+{
+ Name: Defective,
+ DisplayOrder: 1,
+ Id: mrr-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.OrderStatusModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.OrderStatusModel_ToDomain.verified.txt
new file mode 100644
index 000000000..1d84791a6
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.OrderStatusModel_ToDomain.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: Processing,
+ DisplayOrder: 2,
+ IsSystem: false,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.OrderStatus_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.OrderStatus_ToModel.verified.txt
new file mode 100644
index 000000000..1d1f7d885
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.OrderStatus_ToModel.verified.txt
@@ -0,0 +1,5 @@
+{
+ Name: Processing,
+ DisplayOrder: 2,
+ Id: os-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.PermissionCreateModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.PermissionCreateModel_ToDomain.verified.txt
new file mode 100644
index 000000000..15f9946c7
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.PermissionCreateModel_ToDomain.verified.txt
@@ -0,0 +1,7 @@
+{
+ Name: Manage Products,
+ SystemName: ManageProducts,
+ Area: Admin,
+ Category: Catalog,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Permission_ToUpdateModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Permission_ToUpdateModel.verified.txt
new file mode 100644
index 000000000..542a6b199
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Permission_ToUpdateModel.verified.txt
@@ -0,0 +1,7 @@
+{
+ Name: Manage Products,
+ SystemName: ManageProducts,
+ Area: Admin,
+ Category: Catalog,
+ Id: perm-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.PickupPointModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.PickupPointModel_ToDomain.verified.txt
new file mode 100644
index 000000000..c519b9227
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.PickupPointModel_ToDomain.verified.txt
@@ -0,0 +1,10 @@
+{
+ Name: Main Store Pickup,
+ Address: {
+ Id: ObjectId_1
+ },
+ StoreId: store-001,
+ PickupFee: 2.0,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.PickupPoint_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.PickupPoint_ToModel.verified.txt
new file mode 100644
index 000000000..67f7da7bb
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.PickupPoint_ToModel.verified.txt
@@ -0,0 +1,40 @@
+{
+ Name: Main Store Pickup,
+ Description: Pickup at main store,
+ AdminComment: Note,
+ Address: {
+ NameEnabled: false,
+ FirstNameEnabled: false,
+ FirstNameRequired: false,
+ LastNameEnabled: false,
+ LastNameRequired: false,
+ EmailEnabled: false,
+ EmailRequired: false,
+ CompanyEnabled: false,
+ CompanyRequired: false,
+ VatNumberEnabled: false,
+ VatNumberRequired: false,
+ CountryEnabled: false,
+ StateProvinceEnabled: false,
+ CityEnabled: false,
+ CityRequired: false,
+ StreetAddressEnabled: false,
+ StreetAddressRequired: false,
+ StreetAddress2Enabled: false,
+ StreetAddress2Required: false,
+ ZipPostalCodeEnabled: false,
+ ZipPostalCodeRequired: false,
+ PhoneEnabled: false,
+ PhoneRequired: false,
+ FaxEnabled: false,
+ FaxRequired: false,
+ NoteEnabled: false,
+ AddressTypeEnabled: false
+ },
+ DisplayOrder: 1,
+ StoreId: store-001,
+ PickupFee: 2.0,
+ Latitude: 40.7128,
+ Longitude: -74.006,
+ Id: pp-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.StateProvinceModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.StateProvinceModel_ToDomain.verified.txt
new file mode 100644
index 000000000..972935134
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.StateProvinceModel_ToDomain.verified.txt
@@ -0,0 +1,7 @@
+{
+ Name: New York,
+ Abbreviation: NY,
+ Published: true,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.StateProvince_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.StateProvince_ToModel.verified.txt
new file mode 100644
index 000000000..caa44e425
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.StateProvince_ToModel.verified.txt
@@ -0,0 +1,7 @@
+{
+ Name: New York,
+ Abbreviation: NY,
+ Published: true,
+ DisplayOrder: 1,
+ Id: sp-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.StoreModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.StoreModel_ToDomain.verified.txt
new file mode 100644
index 000000000..28ae0fd58
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.StoreModel_ToDomain.verified.txt
@@ -0,0 +1,12 @@
+{
+ Name: Grand Store,
+ Url: https://grandstore.com,
+ SslEnabled: true,
+ DefaultLanguageId: lang-001,
+ DisplayOrder: 1,
+ CompanyName: Grand LLC,
+ BankAccount: {
+ Id: ObjectId_1
+ },
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Store_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Store_ToModel.verified.txt
new file mode 100644
index 000000000..37baa2633
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Store_ToModel.verified.txt
@@ -0,0 +1,26 @@
+{
+ Name: Grand Store,
+ Url: https://grandstore.com,
+ SslEnabled: true,
+ DisplayOrder: 1,
+ CompanyName: Grand LLC,
+ CompanyAddress: 123 Commerce St,
+ CompanyPhoneNumber: +1-555-9999,
+ CompanyVat: US123456,
+ DefaultLanguageId: lang-001,
+ DefaultCountryId: country-001,
+ DefaultCurrencyId: cur-001,
+ Domains: [
+ {
+ Url: https://grandstore.com,
+ Primary: true,
+ Id: dh-001
+ }
+ ],
+ BankAccount: {
+ BankCode: CHASUSU,
+ BankName: Chase,
+ Id: ObjectId_1
+ },
+ Id: store-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.TaxCategoryModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.TaxCategoryModel_ToDomain.verified.txt
new file mode 100644
index 000000000..c7e6388b1
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.TaxCategoryModel_ToDomain.verified.txt
@@ -0,0 +1,5 @@
+{
+ Name: Standard Rate,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.TaxCategory_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.TaxCategory_ToModel.verified.txt
new file mode 100644
index 000000000..bda61aa29
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.TaxCategory_ToModel.verified.txt
@@ -0,0 +1,5 @@
+{
+ Name: Standard Rate,
+ DisplayOrder: 1,
+ Id: tc-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.WarehouseModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.WarehouseModel_ToDomain.verified.txt
new file mode 100644
index 000000000..fefab2adb
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.WarehouseModel_ToDomain.verified.txt
@@ -0,0 +1,10 @@
+{
+ Code: WH-MAIN,
+ Name: Main Warehouse,
+ AdminComment: Primary,
+ Address: {
+ Id: ObjectId_1
+ },
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Warehouse_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Warehouse_ToModel.verified.txt
new file mode 100644
index 000000000..530412588
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.Warehouse_ToModel.verified.txt
@@ -0,0 +1,37 @@
+{
+ Code: WH-MAIN,
+ Name: Main Warehouse,
+ AdminComment: Primary warehouse,
+ Address: {
+ NameEnabled: false,
+ FirstNameEnabled: false,
+ FirstNameRequired: false,
+ LastNameEnabled: false,
+ LastNameRequired: false,
+ EmailEnabled: false,
+ EmailRequired: false,
+ CompanyEnabled: false,
+ CompanyRequired: false,
+ VatNumberEnabled: false,
+ VatNumberRequired: false,
+ CountryEnabled: false,
+ StateProvinceEnabled: false,
+ CityEnabled: false,
+ CityRequired: false,
+ StreetAddressEnabled: false,
+ StreetAddressRequired: false,
+ StreetAddress2Enabled: false,
+ StreetAddress2Required: false,
+ ZipPostalCodeEnabled: false,
+ ZipPostalCodeRequired: false,
+ PhoneEnabled: false,
+ PhoneRequired: false,
+ FaxEnabled: false,
+ FaxRequired: false,
+ NoteEnabled: false,
+ AddressTypeEnabled: false,
+ Id: ObjectId_1
+ },
+ DisplayOrder: 1,
+ Id: wh-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.cs b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.cs
new file mode 100644
index 000000000..19729d55f
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CommonMappingTests.cs
@@ -0,0 +1,594 @@
+using Grand.Mapping;
+using Grand.Domain.Common;
+using Grand.Domain.Directory;
+using Grand.Domain.Documents;
+using Grand.Domain.Localization;
+using Grand.Domain.Orders;
+using Grand.Domain.Permissions;
+using Grand.Domain.Shipping;
+using Grand.Domain.Stores;
+using Grand.Web.AdminShared.Mapper;
+using Grand.Web.AdminShared.Models.Common;
+using Grand.Web.AdminShared.Models.Directory;
+using Grand.Web.AdminShared.Models.Documents;
+using Grand.Web.AdminShared.Models.Localization;
+using Grand.Web.AdminShared.Models.Orders;
+using Grand.Web.AdminShared.Models.Permissions;
+using Grand.Web.AdminShared.Models.Settings;
+using Grand.Web.AdminShared.Models.Shipping;
+using Grand.Web.AdminShared.Models.Stores;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using VerifyMSTest;
+
+namespace Grand.Mapping.Tests.AdminShared;
+
+[TestClass]
+public class CommonMappingTests : VerifyBase
+{
+ private IMapper _mapper;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ var config = new MapperConfiguration(cfg => {
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ });
+ _mapper = config.CreateMapper();
+ }
+
+ // ── Address ───────────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task Address_ToModel()
+ {
+ var source = new Address {
+ Id = "addr-001",
+ FirstName = "John",
+ LastName = "Doe",
+ Email = "john@example.com",
+ Company = "ACME Corp",
+ CountryId = "country-001",
+ StateProvinceId = "state-001",
+ City = "New York",
+ Address1 = "123 Main St",
+ Address2 = "Apt 4B",
+ ZipPostalCode = "10001",
+ PhoneNumber = "+1-555-1234",
+ FaxNumber = "+1-555-5678"
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task AddressModel_ToDomain()
+ {
+ var model = new AddressModel {
+ FirstName = "John",
+ LastName = "Doe",
+ Email = "john@example.com",
+ Company = "ACME Corp",
+ CountryId = "country-001",
+ StateProvinceId = "state-001",
+ City = "New York",
+ Address1 = "123 Main St",
+ Address2 = "Apt 4B",
+ ZipPostalCode = "10001",
+ PhoneNumber = "+1-555-1234"
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── AddressAttribute ──────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task AddressAttribute_ToModel()
+ {
+ var source = new AddressAttribute {
+ Id = "aa-001",
+ Name = "Custom Field",
+ IsRequired = true,
+ AttributeControlTypeId = 1,
+ DisplayOrder = 1
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task AddressAttributeModel_ToDomain()
+ {
+ var model = new AddressAttributeModel {
+ Name = "Custom Field",
+ IsRequired = true,
+ AttributeControlTypeId = 1,
+ DisplayOrder = 1
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── AddressAttributeValue ─────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task AddressAttributeValue_ToModel()
+ {
+ var source = new AddressAttributeValue {
+ Id = "aav-001",
+ Name = "Option A",
+ IsPreSelected = true,
+ DisplayOrder = 1
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task AddressAttributeValueModel_ToDomain()
+ {
+ var model = new AddressAttributeValueModel {
+ Name = "Option A",
+ IsPreSelected = true,
+ DisplayOrder = 1
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── Country ───────────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task Country_ToModel()
+ {
+ var source = new Country {
+ Id = "country-001",
+ Name = "United States",
+ AllowsBilling = true,
+ AllowsShipping = true,
+ TwoLetterIsoCode = "US",
+ ThreeLetterIsoCode = "USA",
+ NumericIsoCode = 840,
+ SubjectToVat = false,
+ Published = true,
+ DisplayOrder = 1,
+ };
+ source.StateProvinces.Add(new StateProvince { Id = "sp-001", Name = "New York" });
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task CountryModel_ToDomain()
+ {
+ var model = new CountryModel {
+ Name = "United States",
+ AllowsBilling = true,
+ AllowsShipping = true,
+ TwoLetterIsoCode = "US",
+ ThreeLetterIsoCode = "USA",
+ NumericIsoCode = 840,
+ Published = true,
+ DisplayOrder = 1,
+ Stores = ["store-001"]
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── StateProvince ─────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task StateProvince_ToModel()
+ {
+ var source = new StateProvince {
+ Id = "sp-001",
+ Name = "New York",
+ Abbreviation = "NY",
+ Published = true,
+ DisplayOrder = 1
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task StateProvinceModel_ToDomain()
+ {
+ var model = new StateProvinceModel {
+ Name = "New York",
+ Abbreviation = "NY",
+ Published = true,
+ DisplayOrder = 1
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── Currency ──────────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task Currency_ToModel()
+ {
+ var source = new Currency {
+ Id = "cur-001",
+ Name = "US Dollar",
+ CurrencyCode = "USD",
+ Rate = 1.0,
+ DisplayLocale = "en-US",
+ CustomFormatting = "${0:N2}",
+ Published = true,
+ DisplayOrder = 1,
+ RoundingTypeId = 0,
+ MidpointRoundId = 0
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task CurrencyModel_ToDomain()
+ {
+ var model = new CurrencyModel {
+ Name = "US Dollar",
+ CurrencyCode = "USD",
+ Rate = 1.0,
+ DisplayLocale = "en-US",
+ Published = true,
+ DisplayOrder = 1,
+ Stores = ["store-001"]
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── Language ──────────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task Language_ToModel()
+ {
+ var source = new Language {
+ Id = "lang-001",
+ Name = "English",
+ LanguageCulture = "en-US",
+ UniqueSeoCode = "en",
+ FlagImageFileName = "us.png",
+ Rtl = false,
+ Published = true,
+ DisplayOrder = 1,
+ DefaultCurrencyId = "cur-001"
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task LanguageModel_ToDomain()
+ {
+ var model = new LanguageModel {
+ Name = "English",
+ LanguageCulture = "en-US",
+ UniqueSeoCode = "en",
+ FlagImageFileName = "us.png",
+ Rtl = false,
+ Published = true,
+ DisplayOrder = 1,
+ Stores = ["store-001"]
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── MeasureDimension ──────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task MeasureDimension_ToModel()
+ {
+ var source = new MeasureDimension { Id = "md-001", Name = "Centimeter", SystemKeyword = "cm", Ratio = 1.0, DisplayOrder = 1 };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task MeasureDimensionModel_ToDomain()
+ {
+ var model = new MeasureDimensionModel { Name = "Centimeter", SystemKeyword = "cm", Ratio = 1.0, DisplayOrder = 1 };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── MeasureWeight ─────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task MeasureWeight_ToModel()
+ {
+ var source = new MeasureWeight { Id = "mw-001", Name = "Kilogram", SystemKeyword = "kg", Ratio = 1.0, DisplayOrder = 1 };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task MeasureWeightModel_ToDomain()
+ {
+ var model = new MeasureWeightModel { Name = "Kilogram", SystemKeyword = "kg", Ratio = 1.0, DisplayOrder = 1 };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── MeasureUnit ───────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task MeasureUnit_ToModel()
+ {
+ var source = new MeasureUnit { Id = "mu-001", Name = "Piece", DisplayOrder = 1 };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task MeasureUnitModel_ToDomain()
+ {
+ var model = new MeasureUnitModel { Name = "Piece", DisplayOrder = 1 };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── Store ─────────────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task Store_ToModel()
+ {
+ var source = new Store {
+ Id = "store-001",
+ Name = "Grand Store",
+ Url = "https://grandstore.com",
+ SslEnabled = true,
+ CompanyName = "Grand LLC",
+ CompanyAddress = "123 Commerce St",
+ CompanyPhoneNumber = "+1-555-9999",
+ CompanyVat = "US123456",
+ DisplayOrder = 1,
+ DefaultLanguageId = "lang-001",
+ DefaultCurrencyId = "cur-001",
+ DefaultCountryId = "country-001",
+ Domains = [new DomainHost { Id = "dh-001", Url = "https://grandstore.com", Primary = true }],
+ BankAccount = new BankAccount { BankName = "Chase", BankCode = "CHASUSU" }
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task StoreModel_ToDomain()
+ {
+ var model = new StoreModel {
+ Name = "Grand Store",
+ Url = "https://grandstore.com",
+ SslEnabled = true,
+ CompanyName = "Grand LLC",
+ DefaultLanguageId = "lang-001",
+ DisplayOrder = 1
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ [TestMethod]
+ public Task DomainHost_ToModel()
+ {
+ var source = new DomainHost { Id = "dh-001", Url = "https://example.com", Primary = true };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task DomainHostModel_ToDomain()
+ {
+ var model = new DomainHostModel { Url = "https://example.com", Primary = true };
+ return Verify(_mapper.Map(model));
+ }
+
+ [TestMethod]
+ public Task BankAccount_ToModel()
+ {
+ var source = new BankAccount { BankName = "Chase", BankCode = "CHASE123", SwiftCode = "CHASUS33", AccountNumber = "123456789" };
+ return Verify(_mapper.Map(source));
+ }
+
+ // ── Document ──────────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task Document_ToModel()
+ {
+ var source = new Document {
+ Id = "doc-001",
+ Name = "Invoice #001",
+ Number = "INV-001",
+ DocumentTypeId = "dt-001",
+ CustomerId = "cust-001",
+ ReferenceId = Reference.Order,
+ StatusId = DocumentStatus.Open,
+ DisplayOrder = 1,
+ Published = true,
+ TotalAmount = 500,
+ OutstandAmount = 0,
+ CurrencyCode = "USD"
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task DocumentModel_ToDomain()
+ {
+ var model = new DocumentModel {
+ Name = "Invoice #001",
+ Number = "INV-001",
+ DocumentTypeId = "dt-001",
+ Published = true,
+ CustomerGroups = ["grp-001"],
+ Stores = ["store-001"]
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── DocumentType ──────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task DocumentType_ToModel()
+ {
+ var source = new DocumentType { Id = "dt-001", Name = "Invoice", Description = "An invoice document", DisplayOrder = 1 };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task DocumentTypeModel_ToDomain()
+ {
+ var model = new DocumentTypeModel { Name = "Invoice", Description = "An invoice document", DisplayOrder = 1 };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── OrderStatus ───────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task OrderStatus_ToModel()
+ {
+ var source = new OrderStatus { Id = "os-001", Name = "Processing", DisplayOrder = 2 };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task OrderStatusModel_ToDomain()
+ {
+ var model = new OrderStatusModel { Name = "Processing", DisplayOrder = 2 };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── MerchandiseReturnAction ───────────────────────────────────────────────
+
+ [TestMethod]
+ public Task MerchandiseReturnAction_ToModel()
+ {
+ var source = new MerchandiseReturnAction { Id = "mra-001", Name = "Refund", DisplayOrder = 1 };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task MerchandiseReturnActionModel_ToDomain()
+ {
+ var model = new MerchandiseReturnActionModel { Name = "Refund", DisplayOrder = 1 };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── MerchandiseReturnReason ───────────────────────────────────────────────
+
+ [TestMethod]
+ public Task MerchandiseReturnReason_ToModel()
+ {
+ var source = new MerchandiseReturnReason { Id = "mrr-001", Name = "Defective", DisplayOrder = 1 };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task MerchandiseReturnReasonModel_ToDomain()
+ {
+ var model = new MerchandiseReturnReasonModel { Name = "Defective", DisplayOrder = 1 };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── PickupPoint ───────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task PickupPoint_ToModel()
+ {
+ var source = new PickupPoint {
+ Id = "pp-001",
+ Name = "Main Store Pickup",
+ Description = "Pickup at main store",
+ AdminComment = "Note",
+ PickupFee = 2.00,
+ DisplayOrder = 1,
+ StoreId = "store-001",
+ Latitude = 40.7128,
+ Longitude = -74.0060
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task PickupPointModel_ToDomain()
+ {
+ var model = new PickupPointModel {
+ Name = "Main Store Pickup",
+ PickupFee = 2.00,
+ DisplayOrder = 1,
+ StoreId = "store-001"
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── Warehouse ─────────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task Warehouse_ToModel()
+ {
+ var source = new Warehouse {
+ Id = "wh-001",
+ Name = "Main Warehouse",
+ AdminComment = "Primary warehouse",
+ Code = "WH-MAIN",
+ DisplayOrder = 1
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task WarehouseModel_ToDomain()
+ {
+ var model = new WarehouseModel {
+ Name = "Main Warehouse",
+ AdminComment = "Primary",
+ Code = "WH-MAIN",
+ DisplayOrder = 1
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── Permission ────────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task PermissionCreateModel_ToDomain()
+ {
+ var model = new PermissionCreateModel {
+ Name = "Manage Products",
+ SystemName = "ManageProducts",
+ Area = "Admin",
+ Category = "Catalog"
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ [TestMethod]
+ public Task Permission_ToUpdateModel()
+ {
+ var source = new Permission {
+ Id = "perm-001",
+ Name = "Manage Products",
+ SystemName = "ManageProducts",
+ Area = "Admin",
+ Category = "Catalog"
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ // ── TaxCategory ───────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task TaxCategory_ToModel()
+ {
+ var source = new Grand.Domain.Tax.TaxCategory { Id = "tc-001", Name = "Standard Rate", DisplayOrder = 1 };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task TaxCategoryModel_ToDomain()
+ {
+ var model = new Grand.Web.AdminShared.Models.Tax.TaxCategoryModel { Name = "Standard Rate", DisplayOrder = 1 };
+ return Verify(_mapper.Map(model));
+ }
+}
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.BlogCategoryModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.BlogCategoryModel_ToDomain.verified.txt
new file mode 100644
index 000000000..d106fec2e
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.BlogCategoryModel_ToDomain.verified.txt
@@ -0,0 +1,10 @@
+{
+ Name: Tech News,
+ SeName: tech-news,
+ DisplayOrder: 1,
+ LimitedToStores: true,
+ Stores: [
+ store-001
+ ],
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.BlogCategory_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.BlogCategory_ToModel.verified.txt
new file mode 100644
index 000000000..32279684d
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.BlogCategory_ToModel.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: Tech News,
+ SeName: tech-news,
+ DisplayOrder: 1,
+ Id: bc-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.BlogPostModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.BlogPostModel_ToDomain.verified.txt
new file mode 100644
index 000000000..7c79ce9c7
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.BlogPostModel_ToDomain.verified.txt
@@ -0,0 +1,12 @@
+{
+ Title: First Post,
+ Body: Content body,
+ BodyOverview: Overview,
+ AllowComments: true,
+ Tags: tech,blog,
+ LimitedToStores: true,
+ Stores: [
+ store-001
+ ],
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.BlogPost_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.BlogPost_ToModel.verified.txt
new file mode 100644
index 000000000..99ace7a99
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.BlogPost_ToModel.verified.txt
@@ -0,0 +1,12 @@
+{
+ Title: First Post,
+ Body: Content body,
+ BodyOverview: Overview,
+ AllowComments: true,
+ Tags: tech,blog,
+ MetaKeywords: blog,
+ MetaDescription: Blog post,
+ MetaTitle: First Post,
+ SeName: first-post,
+ Id: bp-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.BlogSettingsModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.BlogSettingsModel_ToDomain.verified.txt
new file mode 100644
index 000000000..c30091cb8
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.BlogSettingsModel_ToDomain.verified.txt
@@ -0,0 +1,8 @@
+{
+ Enabled: true,
+ PostsPageSize: 10,
+ AllowNotRegisteredUsersToLeaveComments: false,
+ NotifyAboutNewBlogComments: false,
+ ShowBlogOnHomePage: false,
+ ShowBlogPostsInSearchAutoComplete: false
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.BlogSettings_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.BlogSettings_ToModel.verified.txt
new file mode 100644
index 000000000..c30091cb8
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.BlogSettings_ToModel.verified.txt
@@ -0,0 +1,8 @@
+{
+ Enabled: true,
+ PostsPageSize: 10,
+ AllowNotRegisteredUsersToLeaveComments: false,
+ NotifyAboutNewBlogComments: false,
+ ShowBlogOnHomePage: false,
+ ShowBlogPostsInSearchAutoComplete: false
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseLessonModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseLessonModel_ToDomain.verified.txt
new file mode 100644
index 000000000..428f11a96
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseLessonModel_ToDomain.verified.txt
@@ -0,0 +1,9 @@
+{
+ Name: Introduction,
+ ShortDescription: Lesson overview,
+ DisplayOrder: 1,
+ CourseId: course-001,
+ SubjectId: subj-001,
+ Published: true,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseLesson_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseLesson_ToModel.verified.txt
new file mode 100644
index 000000000..4f24f42d7
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseLesson_ToModel.verified.txt
@@ -0,0 +1,11 @@
+{
+ Name: Introduction,
+ ShortDescription: Lesson overview,
+ DisplayOrder: 1,
+ CourseId: course-001,
+ SubjectId: subj-001,
+ VideoFile: intro.mp4,
+ AttachmentId: attach-001,
+ Published: true,
+ Id: lesson-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseLevelModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseLevelModel_ToDomain.verified.txt
new file mode 100644
index 000000000..b8d834834
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseLevelModel_ToDomain.verified.txt
@@ -0,0 +1,5 @@
+{
+ Name: Beginner,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseLevel_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseLevel_ToModel.verified.txt
new file mode 100644
index 000000000..98dc30c12
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseLevel_ToModel.verified.txt
@@ -0,0 +1,5 @@
+{
+ Name: Beginner,
+ DisplayOrder: 1,
+ Id: cl-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseModel_ToDomain.verified.txt
new file mode 100644
index 000000000..608d5aabe
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseModel_ToDomain.verified.txt
@@ -0,0 +1,17 @@
+{
+ Name: C# Fundamentals,
+ Description: Full course,
+ ShortDescription: Learn C#,
+ Published: true,
+ DisplayOrder: 1,
+ LevelId: cl-001,
+ LimitedToGroups: true,
+ CustomerGroups: [
+ grp-001
+ ],
+ LimitedToStores: true,
+ Stores: [
+ store-001
+ ],
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseSubjectModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseSubjectModel_ToDomain.verified.txt
new file mode 100644
index 000000000..1395e2638
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseSubjectModel_ToDomain.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: Chapter 1,
+ DisplayOrder: 1,
+ CourseId: course-001,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseSubject_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseSubject_ToModel.verified.txt
new file mode 100644
index 000000000..61c7a19d9
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.CourseSubject_ToModel.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: Chapter 1,
+ DisplayOrder: 1,
+ CourseId: course-001,
+ Id: subj-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.Course_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.Course_ToModel.verified.txt
new file mode 100644
index 000000000..9eac962eb
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.Course_ToModel.verified.txt
@@ -0,0 +1,11 @@
+{
+ Name: C# Fundamentals,
+ ShortDescription: Learn C#,
+ Description: Full course,
+ DisplayOrder: 1,
+ Published: true,
+ LevelId: cl-001,
+ SeName: csharp-fundamentals,
+ ProductId: prod-001,
+ Id: course-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.KnowledgebaseArticleModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.KnowledgebaseArticleModel_ToDomain.verified.txt
new file mode 100644
index 000000000..7d129edf8
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.KnowledgebaseArticleModel_ToDomain.verified.txt
@@ -0,0 +1,18 @@
+{
+ Content: Step-by-step guide,
+ DisplayOrder: 1,
+ Published: true,
+ ShowOnHomepage: false,
+ AllowComments: false,
+ LimitedToGroups: true,
+ CustomerGroups: [
+ grp-001
+ ],
+ LimitedToStores: true,
+ Stores: [
+ store-001
+ ],
+ Name: How to order?,
+ ParentCategoryId: kb-001,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.KnowledgebaseArticle_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.KnowledgebaseArticle_ToModel.verified.txt
new file mode 100644
index 000000000..bc8e8f705
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.KnowledgebaseArticle_ToModel.verified.txt
@@ -0,0 +1,11 @@
+{
+ Name: How to order?,
+ Content: Step-by-step guide,
+ ParentCategoryId: kb-001,
+ DisplayOrder: 1,
+ Published: true,
+ SeName: how-to-order,
+ ShowOnHomepage: false,
+ AllowComments: false,
+ Id: kba-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.KnowledgebaseCategoryModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.KnowledgebaseCategoryModel_ToDomain.verified.txt
new file mode 100644
index 000000000..86c800b01
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.KnowledgebaseCategoryModel_ToDomain.verified.txt
@@ -0,0 +1,15 @@
+{
+ DisplayOrder: 1,
+ Description: Frequently asked questions,
+ Published: true,
+ LimitedToGroups: true,
+ CustomerGroups: [
+ grp-001
+ ],
+ LimitedToStores: true,
+ Stores: [
+ store-001
+ ],
+ Name: FAQ,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.KnowledgebaseCategory_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.KnowledgebaseCategory_ToModel.verified.txt
new file mode 100644
index 000000000..bad8cb12a
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.KnowledgebaseCategory_ToModel.verified.txt
@@ -0,0 +1,10 @@
+{
+ Name: FAQ,
+ ParentCategoryId: ,
+ DisplayOrder: 1,
+ Description: Frequently asked questions,
+ Published: true,
+ SeName: faq,
+ LimitedToStores: false,
+ Id: kb-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.KnowledgebaseSettingsModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.KnowledgebaseSettingsModel_ToDomain.verified.txt
new file mode 100644
index 000000000..35a128db5
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.KnowledgebaseSettingsModel_ToDomain.verified.txt
@@ -0,0 +1,5 @@
+{
+ Enabled: true,
+ AllowNotRegisteredUsersToLeaveComments: false,
+ NotifyAboutNewArticleComments: false
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.KnowledgebaseSettings_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.KnowledgebaseSettings_ToModel.verified.txt
new file mode 100644
index 000000000..35a128db5
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.KnowledgebaseSettings_ToModel.verified.txt
@@ -0,0 +1,5 @@
+{
+ Enabled: true,
+ AllowNotRegisteredUsersToLeaveComments: false,
+ NotifyAboutNewArticleComments: false
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.NewsItemModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.NewsItemModel_ToDomain.verified.txt
new file mode 100644
index 000000000..e3dc99439
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.NewsItemModel_ToDomain.verified.txt
@@ -0,0 +1,16 @@
+{
+ Title: Breaking News,
+ Short: Short text,
+ Full: Full news content,
+ Published: true,
+ AllowComments: true,
+ LimitedToGroups: true,
+ CustomerGroups: [
+ grp-001
+ ],
+ LimitedToStores: true,
+ Stores: [
+ store-001
+ ],
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.NewsItem_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.NewsItem_ToModel.verified.txt
new file mode 100644
index 000000000..988abc9ef
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.NewsItem_ToModel.verified.txt
@@ -0,0 +1,12 @@
+{
+ Title: Breaking News,
+ Short: Short text,
+ Full: Full news content,
+ AllowComments: true,
+ MetaKeywords: news,
+ MetaDescription: News,
+ MetaTitle: Breaking News,
+ SeName: breaking-news,
+ Published: true,
+ Id: ni-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.NewsSettingsModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.NewsSettingsModel_ToDomain.verified.txt
new file mode 100644
index 000000000..52022629d
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.NewsSettingsModel_ToDomain.verified.txt
@@ -0,0 +1,7 @@
+{
+ Enabled: true,
+ AllowNotRegisteredUsersToLeaveComments: false,
+ NotifyAboutNewNewsComments: false,
+ ShowNewsOnMainPage: true,
+ MainPageNewsCount: 3
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.NewsSettings_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.NewsSettings_ToModel.verified.txt
new file mode 100644
index 000000000..50c3472aa
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.NewsSettings_ToModel.verified.txt
@@ -0,0 +1,7 @@
+{
+ Enabled: true,
+ AllowNotRegisteredUsersToLeaveComments: true,
+ NotifyAboutNewNewsComments: false,
+ ShowNewsOnMainPage: true,
+ MainPageNewsCount: 3
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.PageModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.PageModel_ToDomain.verified.txt
new file mode 100644
index 000000000..d52085a2d
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.PageModel_ToDomain.verified.txt
@@ -0,0 +1,23 @@
+{
+ SystemName: about-us,
+ IncludeInSitemap: true,
+ IncludeInMenu: false,
+ IncludeInFooterRow1: false,
+ IncludeInFooterRow2: false,
+ IncludeInFooterRow3: false,
+ DisplayOrder: 1,
+ AccessibleWhenStoreClosed: false,
+ IsPasswordProtected: false,
+ Title: About Us,
+ Body: About us page
,
+ Published: true,
+ LimitedToGroups: true,
+ CustomerGroups: [
+ grp-001
+ ],
+ LimitedToStores: true,
+ Stores: [
+ store-001
+ ],
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.Page_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.Page_ToModel.verified.txt
new file mode 100644
index 000000000..de87b088d
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.Page_ToModel.verified.txt
@@ -0,0 +1,20 @@
+{
+ SystemName: about-us,
+ IncludeInSitemap: true,
+ IncludeInMenu: false,
+ IncludeInFooterRow1: false,
+ IncludeInFooterRow2: false,
+ IncludeInFooterRow3: false,
+ DisplayOrder: 1,
+ AccessibleWhenStoreClosed: false,
+ IsPasswordProtected: false,
+ Published: true,
+ Title: About Us,
+ Body: About us page
,
+ PageLayoutId: pl-001,
+ MetaKeywords: about,
+ MetaDescription: About us,
+ MetaTitle: About Us,
+ SeName: about-us,
+ Id: page-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.cs b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.cs
new file mode 100644
index 000000000..3667332bf
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/ContentMappingTests.cs
@@ -0,0 +1,390 @@
+using Grand.Mapping;
+using Grand.Domain.Blogs;
+using Grand.Domain.Courses;
+using Grand.Domain.Knowledgebase;
+using Grand.Domain.News;
+using Grand.Domain.Pages;
+using Grand.Web.AdminShared.Mapper;
+using Grand.Web.AdminShared.Models.Blogs;
+using Grand.Web.AdminShared.Models.Courses;
+using Grand.Web.AdminShared.Models.Knowledgebase;
+using Grand.Web.AdminShared.Models.News;
+using Grand.Web.AdminShared.Models.Pages;
+using Grand.Web.AdminShared.Models.Settings;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using VerifyMSTest;
+
+namespace Grand.Mapping.Tests.AdminShared;
+
+[TestClass]
+public class ContentMappingTests : VerifyBase
+{
+ private IMapper _mapper;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ var config = new MapperConfiguration(cfg => {
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ });
+ _mapper = config.CreateMapper();
+ }
+
+ // ── BlogCategory ──────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task BlogCategory_ToModel()
+ {
+ var source = new BlogCategory {
+ Id = "bc-001",
+ Name = "Tech News",
+ SeName = "tech-news",
+ DisplayOrder = 1
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task BlogCategoryModel_ToDomain()
+ {
+ var model = new BlogCategoryModel {
+ Name = "Tech News",
+ SeName = "tech-news",
+ DisplayOrder = 1,
+ Stores = ["store-001"]
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── BlogPost ──────────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task BlogPost_ToModel()
+ {
+ var source = new BlogPost {
+ Id = "bp-001",
+ Title = "First Post",
+ Body = "Content body",
+ BodyOverview = "Overview",
+ AllowComments = true,
+ Tags = "tech,blog",
+ StartDateUtc = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc),
+ EndDateUtc = new DateTime(2024, 12, 31, 0, 0, 0, DateTimeKind.Utc),
+ MetaKeywords = "blog",
+ MetaDescription = "Blog post",
+ MetaTitle = "First Post",
+ SeName = "first-post"
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task BlogPostModel_ToDomain()
+ {
+ var model = new BlogPostModel {
+ Title = "First Post",
+ Body = "Content body",
+ BodyOverview = "Overview",
+ AllowComments = true,
+ Tags = "tech,blog",
+ Stores = ["store-001"]
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── BlogSettings ──────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task BlogSettings_ToModel()
+ {
+ var source = new BlogSettings { Enabled = true, PostsPageSize = 10, AllowNotRegisteredUsersToLeaveComments = false };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task BlogSettingsModel_ToDomain()
+ {
+ var model = new ContentSettingsModel.BlogSettingsModel { Enabled = true, PostsPageSize = 10 };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── NewsItem ──────────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task NewsItem_ToModel()
+ {
+ var source = new NewsItem {
+ Id = "ni-001",
+ Title = "Breaking News",
+ Short = "Short text",
+ Full = "Full news content",
+ Published = true,
+ AllowComments = true,
+ StartDateUtc = new DateTime(2024, 6, 1, 0, 0, 0, DateTimeKind.Utc),
+ EndDateUtc = new DateTime(2024, 6, 30, 0, 0, 0, DateTimeKind.Utc),
+ MetaKeywords = "news",
+ MetaDescription = "News",
+ MetaTitle = "Breaking News",
+ SeName = "breaking-news"
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task NewsItemModel_ToDomain()
+ {
+ var model = new NewsItemModel {
+ Title = "Breaking News",
+ Short = "Short text",
+ Full = "Full news content",
+ Published = true,
+ AllowComments = true,
+ CustomerGroups = ["grp-001"],
+ Stores = ["store-001"]
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── NewsSettings ──────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task NewsSettings_ToModel()
+ {
+ var source = new NewsSettings { Enabled = true, AllowNotRegisteredUsersToLeaveComments = true, NotifyAboutNewNewsComments = false, ShowNewsOnMainPage = true, MainPageNewsCount = 3 };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task NewsSettingsModel_ToDomain()
+ {
+ var model = new ContentSettingsModel.NewsSettingsModel { Enabled = true, ShowNewsOnMainPage = true, MainPageNewsCount = 3 };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── Page ──────────────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task Page_ToModel()
+ {
+ var source = new Page {
+ Id = "page-001",
+ SystemName = "about-us",
+ IncludeInSitemap = true,
+ IsPasswordProtected = false,
+ Published = true,
+ Title = "About Us",
+ Body = "About us page
",
+ MetaKeywords = "about",
+ MetaDescription = "About us",
+ MetaTitle = "About Us",
+ DisplayOrder = 1,
+ SeName = "about-us",
+ PageLayoutId = "pl-001"
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task PageModel_ToDomain()
+ {
+ var model = new PageModel {
+ SystemName = "about-us",
+ IncludeInSitemap = true,
+ Published = true,
+ Title = "About Us",
+ Body = "About us page
",
+ DisplayOrder = 1,
+ CustomerGroups = ["grp-001"],
+ Stores = ["store-001"]
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── KnowledgebaseCategory ─────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task KnowledgebaseCategory_ToModel()
+ {
+ var source = new KnowledgebaseCategory {
+ Id = "kb-001",
+ Name = "FAQ",
+ Description = "Frequently asked questions",
+ ParentCategoryId = "",
+ DisplayOrder = 1,
+ Published = true,
+ SeName = "faq"
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task KnowledgebaseCategoryModel_ToDomain()
+ {
+ var model = new KnowledgebaseCategoryModel {
+ Name = "FAQ",
+ Description = "Frequently asked questions",
+ DisplayOrder = 1,
+ Published = true,
+ Stores = ["store-001"],
+ CustomerGroups = ["grp-001"]
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ [TestMethod]
+ public Task KnowledgebaseArticle_ToModel()
+ {
+ var source = new KnowledgebaseArticle {
+ Id = "kba-001",
+ Name = "How to order?",
+ Content = "Step-by-step guide",
+ ParentCategoryId = "kb-001",
+ DisplayOrder = 1,
+ Published = true,
+ SeName = "how-to-order"
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task KnowledgebaseArticleModel_ToDomain()
+ {
+ var model = new KnowledgebaseArticleModel {
+ Name = "How to order?",
+ Content = "Step-by-step guide",
+ ParentCategoryId = "kb-001",
+ DisplayOrder = 1,
+ Published = true,
+ Stores = ["store-001"],
+ CustomerGroups = ["grp-001"]
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── KnowledgebaseSettings ─────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task KnowledgebaseSettings_ToModel()
+ {
+ var source = new KnowledgebaseSettings { Enabled = true, AllowNotRegisteredUsersToLeaveComments = false };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task KnowledgebaseSettingsModel_ToDomain()
+ {
+ var model = new ContentSettingsModel.KnowledgebaseSettingsModel { Enabled = true };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── Course ────────────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task Course_ToModel()
+ {
+ var source = new Course {
+ Id = "course-001",
+ Name = "C# Fundamentals",
+ ShortDescription = "Learn C#",
+ Description = "Full course",
+ LevelId = "cl-001",
+ Published = true,
+ DisplayOrder = 1,
+ SeName = "csharp-fundamentals",
+ ProductId = "prod-001"
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task CourseModel_ToDomain()
+ {
+ var model = new CourseModel {
+ Name = "C# Fundamentals",
+ ShortDescription = "Learn C#",
+ Description = "Full course",
+ LevelId = "cl-001",
+ Published = true,
+ DisplayOrder = 1,
+ CustomerGroups = ["grp-001"],
+ Stores = ["store-001"]
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── CourseLevel ───────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task CourseLevel_ToModel()
+ {
+ var source = new CourseLevel { Id = "cl-001", Name = "Beginner", DisplayOrder = 1 };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task CourseLevelModel_ToDomain()
+ {
+ var model = new CourseLevelModel { Name = "Beginner", DisplayOrder = 1 };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── CourseLesson ──────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task CourseLesson_ToModel()
+ {
+ var source = new CourseLesson {
+ Id = "lesson-001",
+ Name = "Introduction",
+ ShortDescription = "Lesson overview",
+ SubjectId = "subj-001",
+ CourseId = "course-001",
+ DisplayOrder = 1,
+ Published = true,
+ AttachmentId = "attach-001",
+ VideoFile = "intro.mp4"
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task CourseLessonModel_ToDomain()
+ {
+ var model = new CourseLessonModel {
+ Name = "Introduction",
+ ShortDescription = "Lesson overview",
+ SubjectId = "subj-001",
+ CourseId = "course-001",
+ DisplayOrder = 1,
+ Published = true
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── CourseSubject ─────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task CourseSubject_ToModel()
+ {
+ var source = new CourseSubject { Id = "subj-001", Name = "Chapter 1", CourseId = "course-001", DisplayOrder = 1 };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task CourseSubjectModel_ToDomain()
+ {
+ var model = new CourseSubjectModel { Name = "Chapter 1", CourseId = "course-001", DisplayOrder = 1 };
+ return Verify(_mapper.Map(model));
+ }
+}
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.ContactAttributeModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.ContactAttributeModel_ToDomain.verified.txt
new file mode 100644
index 000000000..d1cc0e0e6
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.ContactAttributeModel_ToDomain.verified.txt
@@ -0,0 +1,16 @@
+{
+ Name: Preferred Contact,
+ IsRequired: false,
+ AttributeControlTypeId: 1,
+ DisplayOrder: 1,
+ AttributeControlType: DropdownList,
+ LimitedToGroups: true,
+ CustomerGroups: [
+ grp-001
+ ],
+ LimitedToStores: true,
+ Stores: [
+ store-001
+ ],
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.ContactAttributeValueModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.ContactAttributeValueModel_ToDomain.verified.txt
new file mode 100644
index 000000000..f129dde24
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.ContactAttributeValueModel_ToDomain.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: Email,
+ IsPreSelected: true,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.ContactAttributeValue_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.ContactAttributeValue_ToModel.verified.txt
new file mode 100644
index 000000000..2c5c6fc04
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.ContactAttributeValue_ToModel.verified.txt
@@ -0,0 +1,7 @@
+{
+ Name: Email,
+ DisplayColorSquaresRgb: false,
+ IsPreSelected: true,
+ DisplayOrder: 1,
+ Id: coav-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.ContactAttribute_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.ContactAttribute_ToModel.verified.txt
new file mode 100644
index 000000000..54aec7598
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.ContactAttribute_ToModel.verified.txt
@@ -0,0 +1,9 @@
+{
+ Name: Preferred Contact,
+ TextPrompt: How would you like to be contacted?,
+ IsRequired: false,
+ AttributeControlTypeId: 1,
+ DisplayOrder: 1,
+ ConditionAllowed: false,
+ Id: coa-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.ContactUs_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.ContactUs_ToModel.verified.txt
new file mode 100644
index 000000000..3bafb5aac
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.ContactUs_ToModel.verified.txt
@@ -0,0 +1,8 @@
+{
+ Id: cu-001,
+ Email: customer@example.com,
+ FullName: Jane Doe,
+ IpAddress: 192.168.1.1,
+ Subject: Order inquiry,
+ Enquiry: When will my order arrive?
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerAttributeModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerAttributeModel_ToDomain.verified.txt
new file mode 100644
index 000000000..60adbf5d0
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerAttributeModel_ToDomain.verified.txt
@@ -0,0 +1,8 @@
+{
+ Name: Newsletter Preference,
+ IsRequired: false,
+ IsReadOnly: false,
+ AttributeControlTypeId: RadioList,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerAttributeValueModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerAttributeValueModel_ToDomain.verified.txt
new file mode 100644
index 000000000..1e042f157
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerAttributeValueModel_ToDomain.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: Weekly,
+ IsPreSelected: false,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerAttributeValue_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerAttributeValue_ToModel.verified.txt
new file mode 100644
index 000000000..9bb22f976
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerAttributeValue_ToModel.verified.txt
@@ -0,0 +1,6 @@
+{
+ Name: Weekly,
+ IsPreSelected: false,
+ DisplayOrder: 1,
+ Id: cav-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerAttribute_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerAttribute_ToModel.verified.txt
new file mode 100644
index 000000000..9725c2340
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerAttribute_ToModel.verified.txt
@@ -0,0 +1,8 @@
+{
+ Name: Newsletter Preference,
+ IsRequired: false,
+ IsReadOnly: false,
+ AttributeControlTypeId: 2,
+ DisplayOrder: 1,
+ Id: ca-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerGroupModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerGroupModel_ToDomain.verified.txt
new file mode 100644
index 000000000..8783e58e1
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerGroupModel_ToDomain.verified.txt
@@ -0,0 +1,10 @@
+{
+ Name: VIP Customers,
+ FreeShipping: true,
+ TaxExempt: false,
+ Active: true,
+ IsSystem: false,
+ SystemName: vip,
+ EnablePasswordLifetime: false,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerGroup_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerGroup_ToModel.verified.txt
new file mode 100644
index 000000000..75317b7e6
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerGroup_ToModel.verified.txt
@@ -0,0 +1,10 @@
+{
+ Name: VIP Customers,
+ FreeShipping: true,
+ TaxExempt: false,
+ Active: true,
+ IsSystem: false,
+ SystemName: vip,
+ EnablePasswordLifetime: false,
+ Id: cg-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerTagModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerTagModel_ToDomain.verified.txt
new file mode 100644
index 000000000..264423ebf
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerTagModel_ToDomain.verified.txt
@@ -0,0 +1,4 @@
+{
+ Name: BigSpender,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerTag_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerTag_ToModel.verified.txt
new file mode 100644
index 000000000..304206b55
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.CustomerTag_ToModel.verified.txt
@@ -0,0 +1,4 @@
+{
+ Name: BigSpender,
+ Id: ct-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.NewsLetterSubscriptionModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.NewsLetterSubscriptionModel_ToDomain.verified.txt
new file mode 100644
index 000000000..592844ad1
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.NewsLetterSubscriptionModel_ToDomain.verified.txt
@@ -0,0 +1,5 @@
+{
+ Email: subscriber@example.com,
+ Active: true,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.NewsLetterSubscription_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.NewsLetterSubscription_ToModel.verified.txt
new file mode 100644
index 000000000..0b962b977
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.NewsLetterSubscription_ToModel.verified.txt
@@ -0,0 +1,5 @@
+{
+ Email: subscriber@example.com,
+ Active: true,
+ Id: nls-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.NewsletterCategoryModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.NewsletterCategoryModel_ToDomain.verified.txt
new file mode 100644
index 000000000..154ded9eb
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.NewsletterCategoryModel_ToDomain.verified.txt
@@ -0,0 +1,11 @@
+{
+ Name: Tech Updates,
+ Description: Technology newsletter,
+ Selected: false,
+ DisplayOrder: 1,
+ LimitedToStores: true,
+ Stores: [
+ store-001
+ ],
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.NewsletterCategory_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.NewsletterCategory_ToModel.verified.txt
new file mode 100644
index 000000000..757872f4f
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.NewsletterCategory_ToModel.verified.txt
@@ -0,0 +1,7 @@
+{
+ Name: Tech Updates,
+ Description: Technology newsletter,
+ Selected: false,
+ DisplayOrder: 1,
+ Id: nlc-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.SalesEmployeeModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.SalesEmployeeModel_ToDomain.verified.txt
new file mode 100644
index 000000000..82aeba7f4
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.SalesEmployeeModel_ToDomain.verified.txt
@@ -0,0 +1,7 @@
+{
+ Name: Alice Smith,
+ Active: true,
+ Commission: 5.0,
+ DisplayOrder: 1,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.SalesEmployee_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.SalesEmployee_ToModel.verified.txt
new file mode 100644
index 000000000..fa8e61339
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.SalesEmployee_ToModel.verified.txt
@@ -0,0 +1,7 @@
+{
+ Name: Alice Smith,
+ Active: true,
+ Commission: 5.0,
+ DisplayOrder: 1,
+ Id: se-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.UserApiCreateModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.UserApiCreateModel_ToDomain.verified.txt
new file mode 100644
index 000000000..c91921359
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.UserApiCreateModel_ToDomain.verified.txt
@@ -0,0 +1,6 @@
+{
+ Email: newapi@example.com,
+ Password: Secret123!,
+ IsActive: true,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.UserApiModel_ToDomain.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.UserApiModel_ToDomain.verified.txt
new file mode 100644
index 000000000..5bd3ec495
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.UserApiModel_ToDomain.verified.txt
@@ -0,0 +1,5 @@
+{
+ Email: api@example.com,
+ IsActive: true,
+ Id: ObjectId_1
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.UserApi_ToModel.verified.txt b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.UserApi_ToModel.verified.txt
new file mode 100644
index 000000000..41063bd76
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.UserApi_ToModel.verified.txt
@@ -0,0 +1,5 @@
+{
+ Email: api@example.com,
+ IsActive: true,
+ Id: ua-001
+}
\ No newline at end of file
diff --git a/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.cs b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.cs
new file mode 100644
index 000000000..edf927c7c
--- /dev/null
+++ b/src/Tests/Grand.Mapping.Tests/AdminShared/CustomerMappingTests.cs
@@ -0,0 +1,319 @@
+using Grand.Mapping;
+using Grand.Domain.Catalog;
+using Grand.Domain.Customers;
+using Grand.Domain.Messages;
+using Grand.Web.AdminShared.Mapper;
+using Grand.Web.AdminShared.Models.Customers;
+using Grand.Web.AdminShared.Models.Messages;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using VerifyMSTest;
+
+namespace Grand.Mapping.Tests.AdminShared;
+
+[TestClass]
+public class CustomerMappingTests : VerifyBase
+{
+ private IMapper _mapper;
+
+ [TestInitialize]
+ public void Setup()
+ {
+ var config = new MapperConfiguration(cfg => {
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ cfg.AddProfile();
+ });
+ _mapper = config.CreateMapper();
+ }
+
+ // ── CustomerGroup ─────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task CustomerGroup_ToModel()
+ {
+ var source = new CustomerGroup {
+ Id = "cg-001",
+ Name = "VIP Customers",
+ FreeShipping = true,
+ TaxExempt = false,
+ Active = true,
+ IsSystem = false,
+ SystemName = "vip",
+ EnablePasswordLifetime = false
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task CustomerGroupModel_ToDomain()
+ {
+ var model = new CustomerGroupModel {
+ Name = "VIP Customers",
+ FreeShipping = true,
+ TaxExempt = false,
+ Active = true,
+ SystemName = "vip"
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── CustomerAttribute ─────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task CustomerAttribute_ToModel()
+ {
+ var source = new CustomerAttribute {
+ Id = "ca-001",
+ Name = "Newsletter Preference",
+ IsRequired = false,
+ AttributeControlTypeId = (AttributeControlType)2,
+ DisplayOrder = 1
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task CustomerAttributeModel_ToDomain()
+ {
+ var model = new CustomerAttributeModel {
+ Name = "Newsletter Preference",
+ IsRequired = false,
+ AttributeControlTypeId = 2,
+ DisplayOrder = 1
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── CustomerAttributeValue ────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task CustomerAttributeValue_ToModel()
+ {
+ var source = new CustomerAttributeValue {
+ Id = "cav-001",
+ Name = "Weekly",
+ IsPreSelected = false,
+ DisplayOrder = 1
+ };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task CustomerAttributeValueModel_ToDomain()
+ {
+ var model = new CustomerAttributeValueModel {
+ Name = "Weekly",
+ IsPreSelected = false,
+ DisplayOrder = 1
+ };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── CustomerTag ───────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task CustomerTag_ToModel()
+ {
+ var source = new CustomerTag { Id = "ct-001", Name = "BigSpender" };
+ return Verify(_mapper.Map(source));
+ }
+
+ [TestMethod]
+ public Task CustomerTagModel_ToDomain()
+ {
+ var model = new CustomerTagModel { Name = "BigSpender" };
+ return Verify(_mapper.Map(model));
+ }
+
+ // ── SalesEmployee ─────────────────────────────────────────────────────────
+
+ [TestMethod]
+ public Task SalesEmployee_ToModel()
+ {
+ var source = new SalesEmployee {
+ Id = "se-001",
+ Name = "Alice Smith",
+ Active = true,
+ Commission = 5.0,
+ DisplayOrder = 1
+ };
+ return Verify(_mapper.Map