From a68a3b976aa8aa6e13c33fc5638abae002f5ca74 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Wed, 14 Jan 2026 19:49:29 +0700 Subject: [PATCH] feat: Enhance member profile management with gender and preferences - Updated CreateMemberCommand and UpdateMemberProfileCommand to include optional gender and preferences fields. - Modified Member entity to store gender and preferences, improving member data handling. - Implemented validation for gender and preferences in command validators. - Adjusted command handlers to support new fields during member creation and profile updates. - Updated unit tests to verify new functionality for gender and preferences management. --- .../Commands/CreateMemberCommand.cs | 12 +-- .../Commands/CreateMemberCommandHandler.cs | 13 +-- .../Commands/UpdateMemberProfileCommand.cs | 30 +++--- .../UpdateMemberProfileCommandHandler.cs | 21 +++- .../Application/Queries/GetMemberByIdQuery.cs | 8 +- .../Queries/GetMemberByIdQueryHandler.cs | 4 +- .../Queries/GetMembersQueryHandler.cs | 4 +- .../CreateMemberCommandValidator.cs | 24 ++--- .../UpdateMemberProfileCommandValidator.cs | 41 ++++---- .../AggregatesModel/MemberAggregate/Member.cs | 98 +++---------------- .../MemberEntityTypeConfiguration.cs | 43 +------- .../Domain/MemberAggregateTests.cs | 61 +++++++++--- 12 files changed, 144 insertions(+), 215 deletions(-) diff --git a/services/membership-service-net/src/MembershipService.API/Application/Commands/CreateMemberCommand.cs b/services/membership-service-net/src/MembershipService.API/Application/Commands/CreateMemberCommand.cs index 5c2e4b88..b2c1f8fd 100644 --- a/services/membership-service-net/src/MembershipService.API/Application/Commands/CreateMemberCommand.cs +++ b/services/membership-service-net/src/MembershipService.API/Application/Commands/CreateMemberCommand.cs @@ -21,16 +21,10 @@ public class CreateMemberCommand : IRequest public string CountryCode { get; set; } = "VN"; /// - /// EN: Phone number (optional). - /// VI: Số điện thoại (tùy chọn). + /// EN: Gender (optional). + /// VI: Giới tính (tùy chọn). /// - public string? PhoneNumber { get; set; } - - /// - /// EN: Avatar URL (optional). - /// VI: URL avatar (tùy chọn). - /// - public string? AvatarUrl { get; set; } + public string? Gender { get; set; } } /// diff --git a/services/membership-service-net/src/MembershipService.API/Application/Commands/CreateMemberCommandHandler.cs b/services/membership-service-net/src/MembershipService.API/Application/Commands/CreateMemberCommandHandler.cs index 83b994f5..f0c70509 100644 --- a/services/membership-service-net/src/MembershipService.API/Application/Commands/CreateMemberCommandHandler.cs +++ b/services/membership-service-net/src/MembershipService.API/Application/Commands/CreateMemberCommandHandler.cs @@ -30,16 +30,9 @@ public class CreateMemberCommandHandler : IRequestHandler /// public Guid MemberId { get; set; } - /// - /// EN: Phone number. - /// VI: Số điện thoại. - /// - public string? PhoneNumber { get; set; } - - /// - /// EN: Avatar URL. - /// VI: URL avatar. - /// - public string? AvatarUrl { get; set; } - - /// - /// EN: Date of birth. - /// VI: Ngày sinh. - /// - public DateOnly? DateOfBirth { get; set; } - /// /// EN: Gender. /// VI: Giới tính. /// public string? Gender { get; set; } + + /// + /// EN: User preferences (JSON string). + /// VI: Preferences của user (chuỗi JSON). + /// + public string? Preferences { get; set; } + + /// + /// EN: Country code (ISO 3166-1 alpha-2). + /// VI: Mã quốc gia (ISO 3166-1 alpha-2). + /// + public string? CountryCode { get; set; } } /// diff --git a/services/membership-service-net/src/MembershipService.API/Application/Commands/UpdateMemberProfileCommandHandler.cs b/services/membership-service-net/src/MembershipService.API/Application/Commands/UpdateMemberProfileCommandHandler.cs index 0214130a..1ba602c4 100644 --- a/services/membership-service-net/src/MembershipService.API/Application/Commands/UpdateMemberProfileCommandHandler.cs +++ b/services/membership-service-net/src/MembershipService.API/Application/Commands/UpdateMemberProfileCommandHandler.cs @@ -28,7 +28,26 @@ public class UpdateMemberProfileCommandHandler : IRequestHandler /// EN: Member DTO. /// VI: DTO member. /// +/// +/// EN: Profile fields (avatar, phone, DOB, address) are managed by IAM Service's UserProfile. +/// VI: Các trường profile (avatar, phone, DOB, address) được quản lý bởi UserProfile của IAM Service. +/// public class MemberDto { public Guid Id { get; set; } public Guid UserId { get; set; } - public string? PhoneNumber { get; set; } - public string? AvatarUrl { get; set; } - public DateOnly? DateOfBirth { get; set; } public string? Gender { get; set; } public string CountryCode { get; set; } = null!; + public string? Preferences { get; set; } public MembershipLevelDto MembershipLevel { get; set; } = null!; public DateTime CreatedAt { get; set; } public DateTime UpdatedAt { get; set; } diff --git a/services/membership-service-net/src/MembershipService.API/Application/Queries/GetMemberByIdQueryHandler.cs b/services/membership-service-net/src/MembershipService.API/Application/Queries/GetMemberByIdQueryHandler.cs index 7433a4f3..69f955f3 100644 --- a/services/membership-service-net/src/MembershipService.API/Application/Queries/GetMemberByIdQueryHandler.cs +++ b/services/membership-service-net/src/MembershipService.API/Application/Queries/GetMemberByIdQueryHandler.cs @@ -33,11 +33,9 @@ public class GetMemberByIdQueryHandler : IRequestHandler x.PhoneNumber) - .MaximumLength(50) - .Matches(@"^\+?[\d\s-]+$") - .When(x => !string.IsNullOrEmpty(x.PhoneNumber)) - .WithMessage("Invalid phone number format"); - - RuleFor(x => x.AvatarUrl) - .MaximumLength(500) - .Must(BeAValidUrl) - .When(x => !string.IsNullOrEmpty(x.AvatarUrl)) - .WithMessage("Invalid URL format"); + RuleFor(x => x.Gender) + .MaximumLength(10) + .Must(BeAValidGender) + .When(x => !string.IsNullOrEmpty(x.Gender)) + .WithMessage("Gender must be Male, Female, or Other"); } - private static bool BeAValidUrl(string? url) + private static bool BeAValidGender(string? gender) { - if (string.IsNullOrEmpty(url)) return true; - return Uri.TryCreate(url, UriKind.Absolute, out var result) - && (result.Scheme == Uri.UriSchemeHttp || result.Scheme == Uri.UriSchemeHttps); + if (string.IsNullOrEmpty(gender)) return true; + var validGenders = new[] { "Male", "Female", "Other" }; + return validGenders.Contains(gender, StringComparer.OrdinalIgnoreCase); } } diff --git a/services/membership-service-net/src/MembershipService.API/Application/Validations/UpdateMemberProfileCommandValidator.cs b/services/membership-service-net/src/MembershipService.API/Application/Validations/UpdateMemberProfileCommandValidator.cs index 9795ac6d..5b145fb2 100644 --- a/services/membership-service-net/src/MembershipService.API/Application/Validations/UpdateMemberProfileCommandValidator.cs +++ b/services/membership-service-net/src/MembershipService.API/Application/Validations/UpdateMemberProfileCommandValidator.cs @@ -15,30 +15,21 @@ public class UpdateMemberProfileCommandValidator : AbstractValidator x.PhoneNumber) - .MaximumLength(50) - .Matches(@"^\+?[\d\s-]+$") - .When(x => !string.IsNullOrEmpty(x.PhoneNumber)) - .WithMessage("Invalid phone number format"); - - RuleFor(x => x.AvatarUrl) - .MaximumLength(500) - .Must(BeAValidUrl) - .When(x => !string.IsNullOrEmpty(x.AvatarUrl)) - .WithMessage("Invalid URL format"); - RuleFor(x => x.Gender) .MaximumLength(10) .Must(BeAValidGender) .When(x => !string.IsNullOrEmpty(x.Gender)) .WithMessage("Gender must be Male, Female, or Other"); - } - private static bool BeAValidUrl(string? url) - { - if (string.IsNullOrEmpty(url)) return true; - return Uri.TryCreate(url, UriKind.Absolute, out var result) - && (result.Scheme == Uri.UriSchemeHttp || result.Scheme == Uri.UriSchemeHttps); + RuleFor(x => x.CountryCode) + .Length(2) + .When(x => !string.IsNullOrEmpty(x.CountryCode)) + .WithMessage("CountryCode must be 2 characters"); + + RuleFor(x => x.Preferences) + .Must(BeValidJson) + .When(x => !string.IsNullOrEmpty(x.Preferences)) + .WithMessage("Preferences must be valid JSON"); } private static bool BeAValidGender(string? gender) @@ -47,4 +38,18 @@ public class UpdateMemberProfileCommandValidator : AbstractValidator /// -/// EN: This entity stores extended profile information for users registered in IAM Service. -/// The ID is the same as UserId from IAM Service to maintain consistency. -/// VI: Entity này lưu trữ thông tin profile mở rộng cho users đã đăng ký trong IAM Service. -/// ID giống với UserId từ IAM Service để duy trì tính nhất quán. +/// EN: This entity stores membership-specific data. Profile information (avatar, phone, address, DOB) +/// is managed by IAM Service's UserProfile. This service only handles membership level, gender, and preferences. +/// VI: Entity này lưu trữ dữ liệu membership. Thông tin profile (avatar, phone, address, DOB) +/// được quản lý bởi UserProfile của IAM Service. Service này chỉ xử lý membership level, gender, và preferences. /// public class Member : Entity, IAggregateRoot { // EN: Private fields for encapsulation // VI: Fields private để đóng gói - private string? _phoneNumber; - private string? _avatarUrl; - private string? _addressLine1; - private string? _addressLine2; - private string? _city; - private string? _state; - private string? _postalCode; private string _countryCode; - private DateOnly? _dateOfBirth; private string? _gender; private int _membershipLevelId; private MembershipLevel _membershipLevel = null!; @@ -40,60 +32,12 @@ public class Member : Entity, IAggregateRoot /// public Guid UserId => Id; - /// - /// EN: Phone number with country code. - /// VI: Số điện thoại kèm mã quốc gia. - /// - public string? PhoneNumber => _phoneNumber; - - /// - /// EN: URL to user's avatar image. - /// VI: URL đến ảnh avatar của user. - /// - public string? AvatarUrl => _avatarUrl; - - /// - /// EN: Address line 1. - /// VI: Địa chỉ dòng 1. - /// - public string? AddressLine1 => _addressLine1; - - /// - /// EN: Address line 2. - /// VI: Địa chỉ dòng 2. - /// - public string? AddressLine2 => _addressLine2; - - /// - /// EN: City name. - /// VI: Tên thành phố. - /// - public string? City => _city; - - /// - /// EN: State or province. - /// VI: Tỉnh hoặc bang. - /// - public string? State => _state; - - /// - /// EN: Postal code. - /// VI: Mã bưu chính. - /// - public string? PostalCode => _postalCode; - /// /// EN: Country code (ISO 3166-1 alpha-2). /// VI: Mã quốc gia (ISO 3166-1 alpha-2). /// public string CountryCode => _countryCode; - /// - /// EN: Date of birth. - /// VI: Ngày sinh. - /// - public DateOnly? DateOfBirth => _dateOfBirth; - /// /// EN: Gender (male, female, other). /// VI: Giới tính (nam, nữ, khác). @@ -151,13 +95,15 @@ public class Member : Entity, IAggregateRoot /// /// User ID from IAM Service / User ID từ IAM Service /// Country code (ISO 3166-1 alpha-2) / Mã quốc gia - public Member(Guid userId, string countryCode = "VN") : this() + /// Gender (optional) / Giới tính (tùy chọn) + public Member(Guid userId, string countryCode = "VN", string? gender = null) : this() { if (userId == Guid.Empty) throw new ArgumentException("User ID cannot be empty", nameof(userId)); Id = userId; _countryCode = countryCode; + _gender = gender; _membershipLevelId = MembershipLevel.Free.Id; _membershipLevel = MembershipLevel.Free; _createdAt = DateTime.UtcNow; @@ -169,18 +115,11 @@ public class Member : Entity, IAggregateRoot } /// - /// EN: Update basic profile information. - /// VI: Cập nhật thông tin profile cơ bản. + /// EN: Update gender. + /// VI: Cập nhật giới tính. /// - public void UpdateProfile( - string? phoneNumber, - string? avatarUrl, - DateOnly? dateOfBirth, - string? gender) + public void UpdateGender(string? gender) { - _phoneNumber = phoneNumber; - _avatarUrl = avatarUrl; - _dateOfBirth = dateOfBirth; _gender = gender; _updatedAt = DateTime.UtcNow; @@ -188,25 +127,14 @@ public class Member : Entity, IAggregateRoot } /// - /// EN: Update address information. - /// VI: Cập nhật thông tin địa chỉ. + /// EN: Update country code. + /// VI: Cập nhật mã quốc gia. /// - public void UpdateAddress( - string? addressLine1, - string? addressLine2, - string? city, - string? state, - string? postalCode, - string countryCode) + public void UpdateCountryCode(string countryCode) { if (string.IsNullOrWhiteSpace(countryCode)) throw new ArgumentException("Country code cannot be empty", nameof(countryCode)); - _addressLine1 = addressLine1; - _addressLine2 = addressLine2; - _city = city; - _state = state; - _postalCode = postalCode; _countryCode = countryCode; _updatedAt = DateTime.UtcNow; diff --git a/services/membership-service-net/src/MembershipService.Infrastructure/EntityConfigurations/MemberEntityTypeConfiguration.cs b/services/membership-service-net/src/MembershipService.Infrastructure/EntityConfigurations/MemberEntityTypeConfiguration.cs index 0531da43..b3d91245 100644 --- a/services/membership-service-net/src/MembershipService.Infrastructure/EntityConfigurations/MemberEntityTypeConfiguration.cs +++ b/services/membership-service-net/src/MembershipService.Infrastructure/EntityConfigurations/MemberEntityTypeConfiguration.cs @@ -22,50 +22,15 @@ public class MemberEntityTypeConfiguration : IEntityTypeConfiguration .HasColumnName("id") .ValueGeneratedNever(); // EN: ID is set from IAM Service / VI: ID được set từ IAM Service - // EN: Phone number - // VI: Số điện thoại - builder.Property("_phoneNumber") - .HasColumnName("phone_number") - .HasMaxLength(50); - - // EN: Avatar URL - // VI: URL avatar - builder.Property("_avatarUrl") - .HasColumnName("avatar_url") - .HasMaxLength(500); - - // EN: Address fields - // VI: Các trường địa chỉ - builder.Property("_addressLine1") - .HasColumnName("address_line_1") - .HasMaxLength(255); - - builder.Property("_addressLine2") - .HasColumnName("address_line_2") - .HasMaxLength(255); - - builder.Property("_city") - .HasColumnName("city") - .HasMaxLength(100); - - builder.Property("_state") - .HasColumnName("state") - .HasMaxLength(100); - - builder.Property("_postalCode") - .HasColumnName("postal_code") - .HasMaxLength(20); - + // EN: Country code + // VI: Mã quốc gia builder.Property("_countryCode") .HasColumnName("country_code") .HasMaxLength(2) .IsRequired(); - // EN: Personal info - // VI: Thông tin cá nhân - builder.Property("_dateOfBirth") - .HasColumnName("date_of_birth"); - + // EN: Gender + // VI: Giới tính builder.Property("_gender") .HasColumnName("gender") .HasMaxLength(10); diff --git a/services/membership-service-net/tests/MembershipService.UnitTests/Domain/MemberAggregateTests.cs b/services/membership-service-net/tests/MembershipService.UnitTests/Domain/MemberAggregateTests.cs index 04fcad2d..1bd94353 100644 --- a/services/membership-service-net/tests/MembershipService.UnitTests/Domain/MemberAggregateTests.cs +++ b/services/membership-service-net/tests/MembershipService.UnitTests/Domain/MemberAggregateTests.cs @@ -42,24 +42,63 @@ public class MemberAggregateTests } [Fact] - public void UpdateProfile_ShouldUpdateFieldsAndRaiseEvent() + public void CreateMember_WithGender_ShouldSetGender() + { + // Arrange + var userId = Guid.NewGuid(); + + // Act + var member = new Member(userId, "VN", "Male"); + + // Assert + member.Gender.Should().Be("Male"); + member.CountryCode.Should().Be("VN"); + } + + [Fact] + public void UpdateGender_ShouldUpdateFieldAndRaiseEvent() { // Arrange var member = new Member(Guid.NewGuid()); - var phone = "+84901234567"; - var avatar = "https://example.com/avatar.jpg"; - var dob = new DateOnly(1990, 1, 1); - var gender = "Male"; + member.ClearDomainEvents(); // Act - member.UpdateProfile(phone, avatar, dob, gender); + member.UpdateGender("Female"); // Assert - member.PhoneNumber.Should().Be(phone); - member.AvatarUrl.Should().Be(avatar); - member.DateOfBirth.Should().Be(dob); - member.Gender.Should().Be(gender); - member.DomainEvents.Should().HaveCount(2); // Created + Updated + member.Gender.Should().Be("Female"); + member.DomainEvents.Should().ContainSingle(); + } + + [Fact] + public void UpdateCountryCode_ShouldUpdateFieldAndRaiseEvent() + { + // Arrange + var member = new Member(Guid.NewGuid()); + member.ClearDomainEvents(); + + // Act + member.UpdateCountryCode("US"); + + // Assert + member.CountryCode.Should().Be("US"); + member.DomainEvents.Should().ContainSingle(); + } + + [Fact] + public void UpdatePreferences_ShouldUpdateFieldAndRaiseEvent() + { + // Arrange + var member = new Member(Guid.NewGuid()); + member.ClearDomainEvents(); + var preferences = "{\"theme\": \"dark\", \"language\": \"vi\"}"; + + // Act + member.UpdatePreferences(preferences); + + // Assert + member.Preferences.Should().Be(preferences); + member.DomainEvents.Should().ContainSingle(); } [Fact]