From 16716fb366d6149942638287d92e82d14b9db04c Mon Sep 17 00:00:00 2001 From: Alex Goris Date: Fri, 9 Sep 2022 23:20:03 +0200 Subject: [PATCH] fix: Change validation to require either email or phone number, not both --- .../Member/AddMember/AddMemberCommand.cs | 4 +- .../Member/EditMember/EditMemberCommand.cs | 7 +- Domain/Member.cs | 4 +- ...akeMemberEmailAndPhoneNullable.Designer.cs | 318 ++++++++++++++++++ ...9211334_MakeMemberEmailAndPhoneNullable.cs | 63 ++++ .../Migrations/HaSpManContextModelSnapshot.cs | 66 ++-- .../SearchMembers/SearchMembersHandler.cs | 4 +- Web/Models/MemberForm.cs | 9 +- Web/Pages/Members/MemberForm.razor | 2 +- .../RequiredIfStringEqualsAttribute.cs | 29 ++ 10 files changed, 458 insertions(+), 48 deletions(-) create mode 100644 Persistence/Migrations/20220909211334_MakeMemberEmailAndPhoneNullable.Designer.cs create mode 100644 Persistence/Migrations/20220909211334_MakeMemberEmailAndPhoneNullable.cs create mode 100644 Web/Validators/RequiredIfStringEqualsAttribute.cs diff --git a/Commands/Handlers/Member/AddMember/AddMemberCommand.cs b/Commands/Handlers/Member/AddMember/AddMemberCommand.cs index 955c2b4b..cb83a214 100644 --- a/Commands/Handlers/Member/AddMember/AddMemberCommand.cs +++ b/Commands/Handlers/Member/AddMember/AddMemberCommand.cs @@ -27,12 +27,12 @@ public AddMemberCommandValidator() .MaximumLength(50); RuleFor(x => x.Email) - .NotEmpty() + .NotEmpty().When(x => string.IsNullOrWhiteSpace(x.PhoneNumber)) .MaximumLength(100) .EmailAddress(); RuleFor(x => x.PhoneNumber) - .NotEmpty() + .NotEmpty().When(x => string.IsNullOrWhiteSpace(x.Email)) .MaximumLength(50); RuleFor(x => x.Address) diff --git a/Commands/Handlers/Member/EditMember/EditMemberCommand.cs b/Commands/Handlers/Member/EditMember/EditMemberCommand.cs index 9f3bb708..5e4353c2 100644 --- a/Commands/Handlers/Member/EditMember/EditMemberCommand.cs +++ b/Commands/Handlers/Member/EditMember/EditMemberCommand.cs @@ -28,11 +28,12 @@ public EditMemberCommandValidator() .MaximumLength(50); RuleFor(x => x.Email) - .NotEmpty() - .MaximumLength(100); + .NotEmpty().When(x => string.IsNullOrWhiteSpace(x.PhoneNumber)) + .MaximumLength(100) + .EmailAddress(); RuleFor(x => x.PhoneNumber) - .NotEmpty() + .NotEmpty().When(x => string.IsNullOrWhiteSpace(x.Email)) .MaximumLength(50); RuleFor(x => x.Address) diff --git a/Domain/Member.cs b/Domain/Member.cs index b84aeb2d..b7b4bccd 100644 --- a/Domain/Member.cs +++ b/Domain/Member.cs @@ -60,8 +60,8 @@ private Member() { } // Make EFCore happy public Guid Id { get; private set; } public string FirstName { get; private set; } public string LastName { get; private set; } - public string Email { get; private set; } - public string PhoneNumber { get; private set; } + public string? Email { get; private set; } + public string? PhoneNumber { get; private set; } public Address Address { get; private set; } public DateTimeOffset? MembershipExpiryDate { get; private set; } public double MembershipFee { get; private set; } diff --git a/Persistence/Migrations/20220909211334_MakeMemberEmailAndPhoneNullable.Designer.cs b/Persistence/Migrations/20220909211334_MakeMemberEmailAndPhoneNullable.Designer.cs new file mode 100644 index 00000000..273b4ba4 --- /dev/null +++ b/Persistence/Migrations/20220909211334_MakeMemberEmailAndPhoneNullable.Designer.cs @@ -0,0 +1,318 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Persistence; + +#nullable disable + +namespace Persistence.Migrations +{ + [DbContext(typeof(HaSpManContext))] + [Migration("20220909211334_MakeMemberEmailAndPhoneNullable")] + partial class MakeMemberEmailAndPhoneNullable + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasDefaultSchema("HaSpMan") + .HasAnnotation("ProductVersion", "6.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1); + + modelBuilder.Entity("Domain.BankAccount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccountNumber") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.HasKey("Id"); + + b.ToTable("BankAccounts", "HaSpMan"); + }); + + modelBuilder.Entity("Domain.Member", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MembershipExpiryDate") + .HasColumnType("datetimeoffset"); + + b.Property("MembershipFee") + .HasColumnType("float"); + + b.Property("PhoneNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id"); + + b.ToTable("Members", "HaSpMan"); + }); + + modelBuilder.Entity("Domain.Transaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b.Property("BankAccountId") + .HasColumnType("uniqueidentifier"); + + b.Property("CounterPartyName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("nvarchar(120)"); + + b.Property("DateFiled") + .HasColumnType("datetimeoffset"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("Discriminator") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("MemberId") + .HasColumnType("uniqueidentifier"); + + b.Property("ReceivedDateTime") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.ToTable("Transactions", "HaSpMan"); + + b.HasDiscriminator("Discriminator").HasValue("Transaction"); + }); + + modelBuilder.Entity("Domain.CreditTransaction", b => + { + b.HasBaseType("Domain.Transaction"); + + b.HasDiscriminator().HasValue("CreditTransaction"); + }); + + modelBuilder.Entity("Domain.DebitTransaction", b => + { + b.HasBaseType("Domain.Transaction"); + + b.HasDiscriminator().HasValue("DebitTransaction"); + }); + + modelBuilder.Entity("Domain.BankAccount", b => + { + b.OwnsMany("Types.AuditEvent", "AuditEvents", b1 => + { + b1.Property("BankAccountId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id"), 1L, 1); + + b1.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b1.Property("PerformedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b1.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b1.HasKey("BankAccountId", "Id"); + + b1.ToTable("BankAccount_AuditEvents", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("BankAccountId"); + }); + + b.Navigation("AuditEvents"); + }); + + modelBuilder.Entity("Domain.Member", b => + { + b.OwnsMany("Types.AuditEvent", "AuditEvents", b1 => + { + b1.Property("MemberId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id"), 1L, 1); + + b1.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b1.Property("PerformedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b1.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b1.HasKey("MemberId", "Id"); + + b1.ToTable("Member_AuditEvents", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("MemberId"); + }); + + b.OwnsOne("Types.Address", "Address", b1 => + { + b1.Property("MemberId") + .HasColumnType("uniqueidentifier"); + + b1.Property("City") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b1.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b1.Property("HouseNumber") + .IsRequired() + .HasMaxLength(15) + .HasColumnType("varchar(15)"); + + b1.Property("Street") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b1.Property("ZipCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b1.HasKey("MemberId"); + + b1.ToTable("Members", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("MemberId"); + }); + + b.Navigation("Address") + .IsRequired(); + + b.Navigation("AuditEvents"); + }); + + modelBuilder.Entity("Domain.Transaction", b => + { + b.OwnsMany("Domain.TransactionAttachment", "Attachments", b1 => + { + b1.Property("TransactionId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id"), 1L, 1); + + b1.Property("FullPath") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b1.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b1.HasKey("TransactionId", "Id"); + + b1.ToTable("Transaction_Attachments", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("TransactionId"); + }); + + b.OwnsMany("Domain.TransactionTypeAmount", "TransactionTypeAmounts", b1 => + { + b1.Property("TransactionId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id"), 1L, 1); + + b1.Property("Amount") + .HasColumnType("decimal(18,2)"); + + b1.Property("TransactionType") + .HasColumnType("int"); + + b1.HasKey("TransactionId", "Id"); + + b1.ToTable("Transaction_TransactionTypeAmounts", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("TransactionId"); + }); + + b.Navigation("Attachments"); + + b.Navigation("TransactionTypeAmounts"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Persistence/Migrations/20220909211334_MakeMemberEmailAndPhoneNullable.cs b/Persistence/Migrations/20220909211334_MakeMemberEmailAndPhoneNullable.cs new file mode 100644 index 00000000..40bb6535 --- /dev/null +++ b/Persistence/Migrations/20220909211334_MakeMemberEmailAndPhoneNullable.cs @@ -0,0 +1,63 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Persistence.Migrations +{ + public partial class MakeMemberEmailAndPhoneNullable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "PhoneNumber", + schema: "HaSpMan", + table: "Members", + type: "varchar(50)", + maxLength: 50, + nullable: true, + oldClrType: typeof(string), + oldType: "varchar(50)", + oldMaxLength: 50); + + migrationBuilder.AlterColumn( + name: "Email", + schema: "HaSpMan", + table: "Members", + type: "varchar(100)", + maxLength: 100, + nullable: true, + oldClrType: typeof(string), + oldType: "varchar(100)", + oldMaxLength: 100); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "PhoneNumber", + schema: "HaSpMan", + table: "Members", + type: "varchar(50)", + maxLength: 50, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "varchar(50)", + oldMaxLength: 50, + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Email", + schema: "HaSpMan", + table: "Members", + type: "varchar(100)", + maxLength: 100, + nullable: false, + defaultValue: "", + oldClrType: typeof(string), + oldType: "varchar(100)", + oldMaxLength: 100, + oldNullable: true); + } + } +} diff --git a/Persistence/Migrations/HaSpManContextModelSnapshot.cs b/Persistence/Migrations/HaSpManContextModelSnapshot.cs index f31a86df..dc8166a5 100644 --- a/Persistence/Migrations/HaSpManContextModelSnapshot.cs +++ b/Persistence/Migrations/HaSpManContextModelSnapshot.cs @@ -51,7 +51,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("uniqueidentifier"); b.Property("Email") - .IsRequired() .HasMaxLength(100) .HasColumnType("varchar(100)"); @@ -72,7 +71,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("float"); b.Property("PhoneNumber") - .IsRequired() .HasMaxLength(50) .HasColumnType("varchar(50)"); @@ -176,6 +174,38 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Domain.Member", b => { + b.OwnsMany("Types.AuditEvent", "AuditEvents", b1 => + { + b1.Property("MemberId") + .HasColumnType("uniqueidentifier"); + + b1.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id"), 1L, 1); + + b1.Property("Description") + .IsRequired() + .HasMaxLength(1000) + .HasColumnType("varchar(1000)"); + + b1.Property("PerformedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b1.Property("Timestamp") + .HasColumnType("datetimeoffset"); + + b1.HasKey("MemberId", "Id"); + + b1.ToTable("Member_AuditEvents", "HaSpMan"); + + b1.WithOwner() + .HasForeignKey("MemberId"); + }); + b.OwnsOne("Types.Address", "Address", b1 => { b1.Property("MemberId") @@ -214,38 +244,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasForeignKey("MemberId"); }); - b.OwnsMany("Types.AuditEvent", "AuditEvents", b1 => - { - b1.Property("MemberId") - .HasColumnType("uniqueidentifier"); - - b1.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("int"); - - SqlServerPropertyBuilderExtensions.UseIdentityColumn(b1.Property("Id"), 1L, 1); - - b1.Property("Description") - .IsRequired() - .HasMaxLength(1000) - .HasColumnType("varchar(1000)"); - - b1.Property("PerformedBy") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("varchar(100)"); - - b1.Property("Timestamp") - .HasColumnType("datetimeoffset"); - - b1.HasKey("MemberId", "Id"); - - b1.ToTable("Member_AuditEvents", "HaSpMan"); - - b1.WithOwner() - .HasForeignKey("MemberId"); - }); - b.Navigation("Address") .IsRequired(); diff --git a/Queries/Members/Handlers/SearchMembers/SearchMembersHandler.cs b/Queries/Members/Handlers/SearchMembers/SearchMembersHandler.cs index df0bee0e..43e40dbf 100644 --- a/Queries/Members/Handlers/SearchMembers/SearchMembersHandler.cs +++ b/Queries/Members/Handlers/SearchMembers/SearchMembersHandler.cs @@ -101,10 +101,10 @@ private static Expression> GetFilterCriteria(string searchStr m.Address.HouseNumber.ToLower().Contains(lowerCaseSearchString) || m.Address.Street.ToLower().Contains(lowerCaseSearchString) || m.Address.ZipCode.ToLower().Contains(lowerCaseSearchString) || - m.Email.ToLower().Contains(lowerCaseSearchString) || + (m.Email != null && m.Email.ToLower().Contains(lowerCaseSearchString)) || m.FirstName.ToLower().Contains(lowerCaseSearchString) || m.LastName.ToLower().Contains(lowerCaseSearchString) || - m.PhoneNumber.ToLower().Contains(lowerCaseSearchString); + (m.PhoneNumber != null && m.PhoneNumber.ToLower().Contains(lowerCaseSearchString)); } } diff --git a/Web/Models/MemberForm.cs b/Web/Models/MemberForm.cs index 9d2ab6c9..781f42b9 100644 --- a/Web/Models/MemberForm.cs +++ b/Web/Models/MemberForm.cs @@ -4,21 +4,22 @@ namespace Web.Models; public class MemberForm { + private string? _email; + [Required] [StringLength(50)] public string? FirstName { get; set; } - [Required] [StringLength(50)] public string? LastName { get; set; } - [Required] + [RequiredIfStringEqualsAttribute(nameof(PhoneNumber), "", ErrorMessage = "Either email or phone number is required")] [StringLength(100)] [EmailAddress] - public string? Email { get; set; } + public string? Email { get => _email; set => _email = value == string.Empty ? null : value; } - [Required] + [RequiredIfStringEqualsAttribute(nameof(Email), "", ErrorMessage = "Either email or phone number is required")] [StringLength(50)] public string? PhoneNumber { get; set; } diff --git a/Web/Pages/Members/MemberForm.razor b/Web/Pages/Members/MemberForm.razor index 158f7510..a0d7e2ab 100644 --- a/Web/Pages/Members/MemberForm.razor +++ b/Web/Pages/Members/MemberForm.razor @@ -15,7 +15,7 @@ + T="string"/>