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.
This commit is contained in:
@@ -21,16 +21,10 @@ public class CreateMemberCommand : IRequest<CreateMemberResult>
|
||||
public string CountryCode { get; set; } = "VN";
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public string? PhoneNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// EN: Avatar URL (optional).
|
||||
/// VI: URL avatar (tùy chọn).
|
||||
/// </summary>
|
||||
public string? AvatarUrl { get; set; }
|
||||
public string? Gender { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -30,16 +30,9 @@ public class CreateMemberCommandHandler : IRequestHandler<CreateMemberCommand, C
|
||||
throw new InvalidOperationException($"Member already exists for user {request.UserId}");
|
||||
}
|
||||
|
||||
// EN: Create new member
|
||||
// VI: Tạo member mới
|
||||
var member = new Member(request.UserId, request.CountryCode);
|
||||
|
||||
// EN: Update optional profile fields
|
||||
// VI: Cập nhật các trường profile tùy chọn
|
||||
if (!string.IsNullOrEmpty(request.PhoneNumber) || !string.IsNullOrEmpty(request.AvatarUrl))
|
||||
{
|
||||
member.UpdateProfile(request.PhoneNumber, request.AvatarUrl, null, null);
|
||||
}
|
||||
// EN: Create new member with gender
|
||||
// VI: Tạo member mới với gender
|
||||
var member = new Member(request.UserId, request.CountryCode, request.Gender);
|
||||
|
||||
_memberRepository.Add(member);
|
||||
await _memberRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
@@ -14,29 +14,23 @@ public class UpdateMemberProfileCommand : IRequest<UpdateMemberProfileResult>
|
||||
/// </summary>
|
||||
public Guid MemberId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// EN: Phone number.
|
||||
/// VI: Số điện thoại.
|
||||
/// </summary>
|
||||
public string? PhoneNumber { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// EN: Avatar URL.
|
||||
/// VI: URL avatar.
|
||||
/// </summary>
|
||||
public string? AvatarUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// EN: Date of birth.
|
||||
/// VI: Ngày sinh.
|
||||
/// </summary>
|
||||
public DateOnly? DateOfBirth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// EN: Gender.
|
||||
/// VI: Giới tính.
|
||||
/// </summary>
|
||||
public string? Gender { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// EN: User preferences (JSON string).
|
||||
/// VI: Preferences của user (chuỗi JSON).
|
||||
/// </summary>
|
||||
public string? Preferences { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// EN: Country code (ISO 3166-1 alpha-2).
|
||||
/// VI: Mã quốc gia (ISO 3166-1 alpha-2).
|
||||
/// </summary>
|
||||
public string? CountryCode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -28,7 +28,26 @@ public class UpdateMemberProfileCommandHandler : IRequestHandler<UpdateMemberPro
|
||||
throw new KeyNotFoundException($"Member {request.MemberId} not found");
|
||||
}
|
||||
|
||||
member.UpdateProfile(request.PhoneNumber, request.AvatarUrl, request.DateOfBirth, request.Gender);
|
||||
// EN: Update gender if provided
|
||||
// VI: Cập nhật gender nếu được cung cấp
|
||||
if (request.Gender != null)
|
||||
{
|
||||
member.UpdateGender(request.Gender);
|
||||
}
|
||||
|
||||
// EN: Update country code if provided
|
||||
// VI: Cập nhật country code nếu được cung cấp
|
||||
if (!string.IsNullOrWhiteSpace(request.CountryCode))
|
||||
{
|
||||
member.UpdateCountryCode(request.CountryCode);
|
||||
}
|
||||
|
||||
// EN: Update preferences if provided
|
||||
// VI: Cập nhật preferences nếu được cung cấp
|
||||
if (request.Preferences != null)
|
||||
{
|
||||
member.UpdatePreferences(request.Preferences);
|
||||
}
|
||||
|
||||
_memberRepository.Update(member);
|
||||
await _memberRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
|
||||
|
||||
@@ -20,15 +20,17 @@ public class GetMemberByIdQuery : IRequest<MemberDto?>
|
||||
/// EN: Member DTO.
|
||||
/// VI: DTO member.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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; }
|
||||
|
||||
@@ -33,11 +33,9 @@ public class GetMemberByIdQueryHandler : IRequestHandler<GetMemberByIdQuery, Mem
|
||||
{
|
||||
Id = member.Id,
|
||||
UserId = member.UserId,
|
||||
PhoneNumber = member.PhoneNumber,
|
||||
AvatarUrl = member.AvatarUrl,
|
||||
DateOfBirth = member.DateOfBirth,
|
||||
Gender = member.Gender,
|
||||
CountryCode = member.CountryCode,
|
||||
Preferences = member.Preferences,
|
||||
MembershipLevel = new MembershipLevelDto
|
||||
{
|
||||
Id = member.MembershipLevel.Id,
|
||||
|
||||
@@ -28,11 +28,9 @@ public class GetMembersQueryHandler : IRequestHandler<GetMembersQuery, GetMember
|
||||
{
|
||||
Id = m.Id,
|
||||
UserId = m.UserId,
|
||||
PhoneNumber = m.PhoneNumber,
|
||||
AvatarUrl = m.AvatarUrl,
|
||||
DateOfBirth = m.DateOfBirth,
|
||||
Gender = m.Gender,
|
||||
CountryCode = m.CountryCode,
|
||||
Preferences = m.Preferences,
|
||||
MembershipLevel = new MembershipLevelDto
|
||||
{
|
||||
Id = m.MembershipLevel.Id,
|
||||
|
||||
@@ -20,23 +20,17 @@ public class CreateMemberCommandValidator : AbstractValidator<CreateMemberComman
|
||||
.Length(2)
|
||||
.WithMessage("CountryCode must be 2 characters");
|
||||
|
||||
RuleFor(x => 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,30 +15,21 @@ public class UpdateMemberProfileCommandValidator : AbstractValidator<UpdateMembe
|
||||
.NotEmpty()
|
||||
.WithMessage("MemberId is required");
|
||||
|
||||
RuleFor(x => 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<UpdateMembe
|
||||
var validGenders = new[] { "Male", "Female", "Other" };
|
||||
return validGenders.Contains(gender, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static bool BeValidJson(string? json)
|
||||
{
|
||||
if (string.IsNullOrEmpty(json)) return true;
|
||||
try
|
||||
{
|
||||
System.Text.Json.JsonDocument.Parse(json);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,24 +8,16 @@ namespace MembershipService.Domain.AggregatesModel.MemberAggregate;
|
||||
/// VI: Member aggregate root - mở rộng thông tin user từ IAM Service.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
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
|
||||
/// </summary>
|
||||
public Guid UserId => Id;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Phone number with country code.
|
||||
/// VI: Số điện thoại kèm mã quốc gia.
|
||||
/// </summary>
|
||||
public string? PhoneNumber => _phoneNumber;
|
||||
|
||||
/// <summary>
|
||||
/// EN: URL to user's avatar image.
|
||||
/// VI: URL đến ảnh avatar của user.
|
||||
/// </summary>
|
||||
public string? AvatarUrl => _avatarUrl;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Address line 1.
|
||||
/// VI: Địa chỉ dòng 1.
|
||||
/// </summary>
|
||||
public string? AddressLine1 => _addressLine1;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Address line 2.
|
||||
/// VI: Địa chỉ dòng 2.
|
||||
/// </summary>
|
||||
public string? AddressLine2 => _addressLine2;
|
||||
|
||||
/// <summary>
|
||||
/// EN: City name.
|
||||
/// VI: Tên thành phố.
|
||||
/// </summary>
|
||||
public string? City => _city;
|
||||
|
||||
/// <summary>
|
||||
/// EN: State or province.
|
||||
/// VI: Tỉnh hoặc bang.
|
||||
/// </summary>
|
||||
public string? State => _state;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Postal code.
|
||||
/// VI: Mã bưu chính.
|
||||
/// </summary>
|
||||
public string? PostalCode => _postalCode;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Country code (ISO 3166-1 alpha-2).
|
||||
/// VI: Mã quốc gia (ISO 3166-1 alpha-2).
|
||||
/// </summary>
|
||||
public string CountryCode => _countryCode;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Date of birth.
|
||||
/// VI: Ngày sinh.
|
||||
/// </summary>
|
||||
public DateOnly? DateOfBirth => _dateOfBirth;
|
||||
|
||||
/// <summary>
|
||||
/// EN: Gender (male, female, other).
|
||||
/// VI: Giới tính (nam, nữ, khác).
|
||||
@@ -151,13 +95,15 @@ public class Member : Entity, IAggregateRoot
|
||||
/// </summary>
|
||||
/// <param name="userId">User ID from IAM Service / User ID từ IAM Service</param>
|
||||
/// <param name="countryCode">Country code (ISO 3166-1 alpha-2) / Mã quốc gia</param>
|
||||
public Member(Guid userId, string countryCode = "VN") : this()
|
||||
/// <param name="gender">Gender (optional) / Giới tính (tùy chọn)</param>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
|
||||
@@ -22,50 +22,15 @@ public class MemberEntityTypeConfiguration : IEntityTypeConfiguration<Member>
|
||||
.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);
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user