From 01c373b0aa45519aa768fe0e44f888d8e137b417 Mon Sep 17 00:00:00 2001 From: Nima Haji Date: Tue, 19 May 2026 00:44:05 +0330 Subject: [PATCH 1/2] start for tenant --- .idea/.idea.Reservation_System/.idea/.name | 1 + .idea/config/applicationhost.config | 2 +- Domain/Entities/teanant.cs | 8 ++++++++ 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .idea/.idea.Reservation_System/.idea/.name create mode 100644 Domain/Entities/teanant.cs diff --git a/.idea/.idea.Reservation_System/.idea/.name b/.idea/.idea.Reservation_System/.idea/.name new file mode 100644 index 0000000..4317306 --- /dev/null +++ b/.idea/.idea.Reservation_System/.idea/.name @@ -0,0 +1 @@ +Reservation_System \ No newline at end of file diff --git a/.idea/config/applicationhost.config b/.idea/config/applicationhost.config index 4257e64..92370fd 100644 --- a/.idea/config/applicationhost.config +++ b/.idea/config/applicationhost.config @@ -156,7 +156,7 @@ - + diff --git a/Domain/Entities/teanant.cs b/Domain/Entities/teanant.cs new file mode 100644 index 0000000..d670f85 --- /dev/null +++ b/Domain/Entities/teanant.cs @@ -0,0 +1,8 @@ +using System.Net; + +namespace Domain.Entities; + +public class teanant +{ + +} \ No newline at end of file From b819871a7e4e3838b667f540e02c6e8709cf5e1c Mon Sep 17 00:00:00 2001 From: Nima Haji Date: Tue, 19 May 2026 15:32:51 +0330 Subject: [PATCH 2/2] Tenant register implemented --- ...ncyInjection.cs => ApplicationServices.cs} | 4 +- .../Tenant/DTO's/RegisterTenantDTO.cs | 8 + .../Interfaces/TenantRepositoryContract.cs | 9 + .../Features/Tenant/Services/TenantService.cs | 24 ++ .../Tenant/Services/TenantServiceContract.cs | 8 + Domain/Entities/Appointment.cs | 2 + Domain/Entities/Service.cs | 2 + Domain/Entities/Tenant.cs | 38 +++ Domain/Entities/User.cs | 2 + Domain/Entities/teanant.cs | 8 - ...Injection.cs => InfrastructureServices.cs} | 6 +- ...4_AddPasswordResetFieldsToUser.Designer.cs | 2 +- .../20260519103444_init.Designer.cs | 317 +++++++++++++++++ .../Migrations/20260519103444_init.cs | 182 ++++++++++ .../20260519111054_NullableTenant.Designer.cs | 318 ++++++++++++++++++ .../20260519111054_NullableTenant.cs | 37 ++ .../AppDbContext.cs | 10 +- .../AppointmentConfiguration.cs | 9 +- .../AppointmentServiceLinkConfiguration.cs | 8 +- .../RefreshTokenConfiguration.cs | 2 +- .../Configurations/ServiceConfiguration.cs | 7 +- .../Configurations/TenantConfiguration.cs | 34 ++ .../Configurations/UserConfiguration.cs | 8 +- .../DbInitializer.cs | 2 +- ...60209204358_InitialCleanSchema.Designer.cs | 2 +- .../20260209204358_InitialCleanSchema.cs | 0 .../Migrations/AppDbContextModelSnapshot.cs | 90 ++++- .../Repositories/AppointmentRepository.cs | 2 +- .../AppointmentServiceLinkRepository.cs | 2 +- .../Repositories/RefreshTokenRepository.cs | 2 +- .../Repositories/ServiceRepository.cs | 2 +- .../Repositories/TenantRepository.cs | 26 ++ .../Repositories/UserRepository.cs | 2 +- api/Controllers/TenantController.cs | 24 ++ api/Program.cs | 2 +- 35 files changed, 1168 insertions(+), 33 deletions(-) rename Application/{DependencyInjection.cs => ApplicationServices.cs} (77%) create mode 100644 Application/Features/Tenant/DTO's/RegisterTenantDTO.cs create mode 100644 Application/Features/Tenant/Interfaces/TenantRepositoryContract.cs create mode 100644 Application/Features/Tenant/Services/TenantService.cs create mode 100644 Application/Features/Tenant/Services/TenantServiceContract.cs create mode 100644 Domain/Entities/Tenant.cs delete mode 100644 Domain/Entities/teanant.cs rename Infrastructure/{DependencyInjection.cs => InfrastructureServices.cs} (89%) create mode 100644 Infrastructure/Migrations/20260519103444_init.Designer.cs create mode 100644 Infrastructure/Migrations/20260519103444_init.cs create mode 100644 Infrastructure/Migrations/20260519111054_NullableTenant.Designer.cs create mode 100644 Infrastructure/Migrations/20260519111054_NullableTenant.cs rename Infrastructure/{Persistance => Persistence}/AppDbContext.cs (71%) rename Infrastructure/{Persistance => Persistence}/Configurations/AppointmentConfiguration.cs (81%) rename Infrastructure/{Persistance => Persistence}/Configurations/AppointmentServiceLinkConfiguration.cs (72%) rename Infrastructure/{Persistance => Persistence}/Configurations/RefreshTokenConfiguration.cs (92%) rename Infrastructure/{Persistance => Persistence}/Configurations/ServiceConfiguration.cs (78%) create mode 100644 Infrastructure/Persistence/Configurations/TenantConfiguration.cs rename Infrastructure/{Persistance => Persistence}/Configurations/UserConfiguration.cs (85%) rename Infrastructure/{Persistance => Persistence}/DbInitializer.cs (95%) rename Infrastructure/{Persistance => Persistence}/Migrations/20260209204358_InitialCleanSchema.Designer.cs (99%) rename Infrastructure/{Persistance => Persistence}/Migrations/20260209204358_InitialCleanSchema.cs (100%) rename Infrastructure/{Persistance => Persistence}/Migrations/AppDbContextModelSnapshot.cs (72%) rename Infrastructure/{Persistance => Persistence}/Repositories/AppointmentRepository.cs (97%) rename Infrastructure/{Persistance => Persistence}/Repositories/AppointmentServiceLinkRepository.cs (96%) rename Infrastructure/{Persistance => Persistence}/Repositories/RefreshTokenRepository.cs (95%) rename Infrastructure/{Persistance => Persistence}/Repositories/ServiceRepository.cs (96%) create mode 100644 Infrastructure/Persistence/Repositories/TenantRepository.cs rename Infrastructure/{Persistance => Persistence}/Repositories/UserRepository.cs (96%) create mode 100644 api/Controllers/TenantController.cs diff --git a/Application/DependencyInjection.cs b/Application/ApplicationServices.cs similarity index 77% rename from Application/DependencyInjection.cs rename to Application/ApplicationServices.cs index 96c9942..7202ade 100644 --- a/Application/DependencyInjection.cs +++ b/Application/ApplicationServices.cs @@ -3,6 +3,7 @@ using Application.Features.Auth.Interfaces; using Application.Features.Auth.Services; using Application.Features.Service.Services; +using Application.Features.Tenant.Services; using Microsoft.Extensions.DependencyInjection; namespace Application; @@ -13,7 +14,8 @@ public static IServiceCollection AddApplication(this IServiceCollection services services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped(); return services; } } \ No newline at end of file diff --git a/Application/Features/Tenant/DTO's/RegisterTenantDTO.cs b/Application/Features/Tenant/DTO's/RegisterTenantDTO.cs new file mode 100644 index 0000000..444208e --- /dev/null +++ b/Application/Features/Tenant/DTO's/RegisterTenantDTO.cs @@ -0,0 +1,8 @@ +namespace Application.Features.Tenant.DTO_s; + +public class RegisterTenantDTO +{ + public string Name { get; set; } + public string Slug { get; set; } + public DateTime SubscriptionExpireDate { get; set; } +} \ No newline at end of file diff --git a/Application/Features/Tenant/Interfaces/TenantRepositoryContract.cs b/Application/Features/Tenant/Interfaces/TenantRepositoryContract.cs new file mode 100644 index 0000000..66d8b4c --- /dev/null +++ b/Application/Features/Tenant/Interfaces/TenantRepositoryContract.cs @@ -0,0 +1,9 @@ +using Application.Features.Tenant.DTO_s; + +namespace Application.Features.Tenant.Interfaces; + +public interface TenantRepositoryContract +{ + Task RegisterTenant(Domain.Entities.Tenant tenant); + Task SaveAsync(); +} \ No newline at end of file diff --git a/Application/Features/Tenant/Services/TenantService.cs b/Application/Features/Tenant/Services/TenantService.cs new file mode 100644 index 0000000..05bffc3 --- /dev/null +++ b/Application/Features/Tenant/Services/TenantService.cs @@ -0,0 +1,24 @@ +using Application.Features.Tenant.DTO_s; +using Application.Features.Tenant.Interfaces; +using Domain.Entities; +namespace Application.Features.Tenant.Services; + +public class TenantService:TenantServiceContract +{ + private readonly TenantRepositoryContract _tenantRepository; + + public TenantService(TenantRepositoryContract tenantRepository) + { + _tenantRepository = tenantRepository; + } + + public async Task Register(RegisterTenantDTO dto) + { + //Todo: slug maker + // Todo : Date manager + var tenant = new Domain.Entities.Tenant(dto.Name,dto.Slug,dto.SubscriptionExpireDate); + await _tenantRepository.RegisterTenant(tenant); + await _tenantRepository.SaveAsync(); + return $"{tenant.Name} Registered"; + } +} \ No newline at end of file diff --git a/Application/Features/Tenant/Services/TenantServiceContract.cs b/Application/Features/Tenant/Services/TenantServiceContract.cs new file mode 100644 index 0000000..6ff6dac --- /dev/null +++ b/Application/Features/Tenant/Services/TenantServiceContract.cs @@ -0,0 +1,8 @@ +using Application.Features.Tenant.DTO_s; + +namespace Application.Features.Tenant.Services; + +public interface TenantServiceContract +{ + Task Register(RegisterTenantDTO dto); +} \ No newline at end of file diff --git a/Domain/Entities/Appointment.cs b/Domain/Entities/Appointment.cs index f2852e1..e673f41 100644 --- a/Domain/Entities/Appointment.cs +++ b/Domain/Entities/Appointment.cs @@ -11,6 +11,8 @@ public class Appointment public DateTime StartTime { get; private set; } public DateTime EndTime { get; private set; } public AppointmentStatus Status { get; private set; } + public Guid TenantId { get; set; } + public Tenant Tenant { get; set; } public ICollection AppointmentServices { get; private set; } = new List(); diff --git a/Domain/Entities/Service.cs b/Domain/Entities/Service.cs index 9aca70f..33612b3 100644 --- a/Domain/Entities/Service.cs +++ b/Domain/Entities/Service.cs @@ -5,6 +5,8 @@ public class Service public Guid Id { get; private set; } public string Title { get; private set; } public int DurationMinutes { get; private set; } + public Guid TenantId { get; set; } + public Tenant Tenant { get; set; } public ICollection AppointmentServices { get; set; }=new List(); private Service() { } diff --git a/Domain/Entities/Tenant.cs b/Domain/Entities/Tenant.cs new file mode 100644 index 0000000..b43d327 --- /dev/null +++ b/Domain/Entities/Tenant.cs @@ -0,0 +1,38 @@ +namespace Domain.Entities; + +public class Tenant +{ + public Guid Id { get; set; } + public string Name { get; set; } = null!; + public DateTime DateJoined { get; set; } + public DateTime ExpireSubscriptionDate { get; set; } + public string Slug { get; set; } = null!; + public SubsciptionStatus SubscriptionStatus { get; set; } + public List Appointments { get; set; } + public List Services { get; set; } + public List Users { get; set; } + + public bool IsSubscriptionValid() + { + return SubscriptionStatus == SubsciptionStatus.Active && ExpireSubscriptionDate > DateTime.UtcNow; + } + + public void ActiveSubscription() => SubscriptionStatus = SubsciptionStatus.Active; + public void DeActiveSubscription() => SubscriptionStatus = SubsciptionStatus.Inactive; + private Tenant(){} + public Tenant(string name,string slug,DateTime expireSubscriptionDate) + { + Id = Guid.NewGuid(); + DateJoined = DateTime.UtcNow; + Name = name; + Slug = slug; + SubscriptionStatus = SubsciptionStatus.Active; + ExpireSubscriptionDate = expireSubscriptionDate; + } +} + +public enum SubsciptionStatus +{ + Active, + Inactive, +} \ No newline at end of file diff --git a/Domain/Entities/User.cs b/Domain/Entities/User.cs index 70602ec..cedf899 100644 --- a/Domain/Entities/User.cs +++ b/Domain/Entities/User.cs @@ -13,6 +13,8 @@ public class User public string? PasswordResetCodeHash { get; private set; } public DateTime? PasswordResetCodeExpireAt { get; private set; } public int PasswordResetAttemptsCount { get; private set; } + public Guid? TenantId { get; private set; } + public Tenant? Tenant { get; set; } public ICollection RefreshTokens { get; private set; } = new List(); public ICollection Appointments { get; private set; } = new List(); diff --git a/Domain/Entities/teanant.cs b/Domain/Entities/teanant.cs deleted file mode 100644 index d670f85..0000000 --- a/Domain/Entities/teanant.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Net; - -namespace Domain.Entities; - -public class teanant -{ - -} \ No newline at end of file diff --git a/Infrastructure/DependencyInjection.cs b/Infrastructure/InfrastructureServices.cs similarity index 89% rename from Infrastructure/DependencyInjection.cs rename to Infrastructure/InfrastructureServices.cs index bc3d3f9..4f9a2b5 100644 --- a/Infrastructure/DependencyInjection.cs +++ b/Infrastructure/InfrastructureServices.cs @@ -5,8 +5,9 @@ using Application.Features.AppointmentServiceLink.Interfaces; using Application.Features.Auth.Interfaces; using Application.Features.Service.Interfaces; -using Infrastructure.Persistance; -using Infrastructure.Persistance.Repositories; +using Application.Features.Tenant.Interfaces; +using Infrastructure.Persistence; +using Infrastructure.Persistence.Repositories; using Infrastructure.Security; using Infrastructure.Security.Context; using Infrastructure.Security.Hashing; @@ -34,6 +35,7 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); return services; } diff --git a/Infrastructure/Migrations/20260220105944_AddPasswordResetFieldsToUser.Designer.cs b/Infrastructure/Migrations/20260220105944_AddPasswordResetFieldsToUser.Designer.cs index 6f11365..9167b94 100644 --- a/Infrastructure/Migrations/20260220105944_AddPasswordResetFieldsToUser.Designer.cs +++ b/Infrastructure/Migrations/20260220105944_AddPasswordResetFieldsToUser.Designer.cs @@ -1,6 +1,6 @@ // using System; -using Infrastructure.Persistance; +using Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; diff --git a/Infrastructure/Migrations/20260519103444_init.Designer.cs b/Infrastructure/Migrations/20260519103444_init.Designer.cs new file mode 100644 index 0000000..00705b5 --- /dev/null +++ b/Infrastructure/Migrations/20260519103444_init.Designer.cs @@ -0,0 +1,317 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Infrastructure.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260519103444_init")] + partial class init + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Appointment", b => + { + b.Property("AppointmentId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("AppointmentId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UserId"); + + b.ToTable("Appointments", t => + { + t.HasCheckConstraint("CK_Status_Valid_Values", "[Status] IN (0, 1,2)"); + }); + }); + + modelBuilder.Entity("Domain.Entities.AppointmentServiceLink", b => + { + b.Property("AppointmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("ServiceId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("AppointmentId", "ServiceId"); + + b.HasIndex("ServiceId"); + + b.ToTable("AppointmentServiceLinks", (string)null); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsRevoked") + .HasColumnType("bit"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Token") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DurationMinutes") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("Service", (string)null); + }); + + modelBuilder.Entity("Domain.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DateJoined") + .HasColumnType("datetime2"); + + b.Property("ExpireSubscriptionDate") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(true) + .HasColumnType("nvarchar(200)"); + + b.Property("SubscriptionStatus") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Tenants", (string)null); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PasswordResetAttemptsCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("PasswordResetCodeExpireAt") + .HasColumnType("datetime2"); + + b.Property("PasswordResetCodeHash") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(11) + .HasColumnType("nvarchar(11)"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("Users", null, t => + { + t.HasCheckConstraint("CK_Role_Valid_Values", "[Role] IN (0, 1)"); + }); + }); + + modelBuilder.Entity("Domain.Entities.Appointment", b => + { + b.HasOne("Domain.Entities.Tenant", "Tenant") + .WithMany("Appointments") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "User") + .WithMany("Appointments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.AppointmentServiceLink", b => + { + b.HasOne("Domain.Entities.Appointment", "Appointment") + .WithMany("AppointmentServices") + .HasForeignKey("AppointmentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Domain.Entities.Service", "Service") + .WithMany("AppointmentServices") + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Appointment"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Service", b => + { + b.HasOne("Domain.Entities.Tenant", "Tenant") + .WithMany("Services") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Tenant", "Tenant") + .WithMany() + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Domain.Entities.Appointment", b => + { + b.Navigation("AppointmentServices"); + }); + + modelBuilder.Entity("Domain.Entities.Service", b => + { + b.Navigation("AppointmentServices"); + }); + + modelBuilder.Entity("Domain.Entities.Tenant", b => + { + b.Navigation("Appointments"); + + b.Navigation("Services"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Navigation("Appointments"); + + b.Navigation("RefreshTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Infrastructure/Migrations/20260519103444_init.cs b/Infrastructure/Migrations/20260519103444_init.cs new file mode 100644 index 0000000..1268c7f --- /dev/null +++ b/Infrastructure/Migrations/20260519103444_init.cs @@ -0,0 +1,182 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Migrations +{ + /// + public partial class init : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_AppointmentServiceLinks_Appointments_AppointmentId", + table: "AppointmentServiceLinks"); + + migrationBuilder.DropForeignKey( + name: "FK_AppointmentServiceLinks_Service_ServiceId", + table: "AppointmentServiceLinks"); + + migrationBuilder.AddColumn( + name: "TenantId", + table: "Users", + type: "uniqueidentifier", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "TenantId", + table: "Service", + type: "uniqueidentifier", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "TenantId", + table: "Appointments", + type: "uniqueidentifier", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.CreateTable( + name: "Tenants", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + DateJoined = table.Column(type: "datetime2", nullable: false), + ExpireSubscriptionDate = table.Column(type: "datetime2", nullable: false), + Slug = table.Column(type: "nvarchar(200)", maxLength: 200, nullable: false), + SubscriptionStatus = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tenants", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Users_TenantId", + table: "Users", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_Service_TenantId", + table: "Service", + column: "TenantId"); + + migrationBuilder.CreateIndex( + name: "IX_Appointments_TenantId", + table: "Appointments", + column: "TenantId"); + + migrationBuilder.AddForeignKey( + name: "FK_Appointments_Tenants_TenantId", + table: "Appointments", + column: "TenantId", + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_AppointmentServiceLinks_Appointments_AppointmentId", + table: "AppointmentServiceLinks", + column: "AppointmentId", + principalTable: "Appointments", + principalColumn: "AppointmentId", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_AppointmentServiceLinks_Service_ServiceId", + table: "AppointmentServiceLinks", + column: "ServiceId", + principalTable: "Service", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Service_Tenants_TenantId", + table: "Service", + column: "TenantId", + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_Users_Tenants_TenantId", + table: "Users", + column: "TenantId", + principalTable: "Tenants", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Appointments_Tenants_TenantId", + table: "Appointments"); + + migrationBuilder.DropForeignKey( + name: "FK_AppointmentServiceLinks_Appointments_AppointmentId", + table: "AppointmentServiceLinks"); + + migrationBuilder.DropForeignKey( + name: "FK_AppointmentServiceLinks_Service_ServiceId", + table: "AppointmentServiceLinks"); + + migrationBuilder.DropForeignKey( + name: "FK_Service_Tenants_TenantId", + table: "Service"); + + migrationBuilder.DropForeignKey( + name: "FK_Users_Tenants_TenantId", + table: "Users"); + + migrationBuilder.DropTable( + name: "Tenants"); + + migrationBuilder.DropIndex( + name: "IX_Users_TenantId", + table: "Users"); + + migrationBuilder.DropIndex( + name: "IX_Service_TenantId", + table: "Service"); + + migrationBuilder.DropIndex( + name: "IX_Appointments_TenantId", + table: "Appointments"); + + migrationBuilder.DropColumn( + name: "TenantId", + table: "Users"); + + migrationBuilder.DropColumn( + name: "TenantId", + table: "Service"); + + migrationBuilder.DropColumn( + name: "TenantId", + table: "Appointments"); + + migrationBuilder.AddForeignKey( + name: "FK_AppointmentServiceLinks_Appointments_AppointmentId", + table: "AppointmentServiceLinks", + column: "AppointmentId", + principalTable: "Appointments", + principalColumn: "AppointmentId", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_AppointmentServiceLinks_Service_ServiceId", + table: "AppointmentServiceLinks", + column: "ServiceId", + principalTable: "Service", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/Infrastructure/Migrations/20260519111054_NullableTenant.Designer.cs b/Infrastructure/Migrations/20260519111054_NullableTenant.Designer.cs new file mode 100644 index 0000000..85b6a11 --- /dev/null +++ b/Infrastructure/Migrations/20260519111054_NullableTenant.Designer.cs @@ -0,0 +1,318 @@ +// +using System; +using Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Infrastructure.Migrations +{ + [DbContext(typeof(AppDbContext))] + [Migration("20260519111054_NullableTenant")] + partial class NullableTenant + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Domain.Entities.Appointment", b => + { + b.Property("AppointmentId") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("EndTime") + .HasColumnType("datetime2"); + + b.Property("StartTime") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("AppointmentId"); + + b.HasIndex("TenantId"); + + b.HasIndex("UserId"); + + b.ToTable("Appointments", t => + { + t.HasCheckConstraint("CK_Status_Valid_Values", "[Status] IN (0, 1,2)"); + }); + }); + + modelBuilder.Entity("Domain.Entities.AppointmentServiceLink", b => + { + b.Property("AppointmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("ServiceId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("AppointmentId", "ServiceId"); + + b.HasIndex("ServiceId"); + + b.ToTable("AppointmentServiceLinks", (string)null); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("IsRevoked") + .HasColumnType("bit"); + + b.Property("Token") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("Token") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("RefreshTokens"); + }); + + modelBuilder.Entity("Domain.Entities.Service", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DurationMinutes") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("Service", (string)null); + }); + + modelBuilder.Entity("Domain.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DateJoined") + .HasColumnType("datetime2"); + + b.Property("ExpireSubscriptionDate") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(true) + .HasColumnType("nvarchar(200)"); + + b.Property("SubscriptionStatus") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Tenants", (string)null); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("FullName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Password") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PasswordResetAttemptsCount") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("PasswordResetCodeExpireAt") + .HasColumnType("datetime2"); + + b.Property("PasswordResetCodeHash") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(11) + .HasColumnType("nvarchar(11)"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("TenantId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.ToTable("Users", null, t => + { + t.HasCheckConstraint("CK_Role_Valid_Values", "[Role] IN (0, 1)"); + }); + }); + + modelBuilder.Entity("Domain.Entities.Appointment", b => + { + b.HasOne("Domain.Entities.Tenant", "Tenant") + .WithMany("Appointments") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Domain.Entities.User", "User") + .WithMany("Appointments") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.AppointmentServiceLink", b => + { + b.HasOne("Domain.Entities.Appointment", "Appointment") + .WithMany("AppointmentServices") + .HasForeignKey("AppointmentId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Domain.Entities.Service", "Service") + .WithMany("AppointmentServices") + .HasForeignKey("ServiceId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Appointment"); + + b.Navigation("Service"); + }); + + modelBuilder.Entity("Domain.Entities.RefreshToken", b => + { + b.HasOne("Domain.Entities.User", "User") + .WithMany("RefreshTokens") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Domain.Entities.Service", b => + { + b.HasOne("Domain.Entities.Tenant", "Tenant") + .WithMany("Services") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Tenant", "Tenant") + .WithMany("Users") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Domain.Entities.Appointment", b => + { + b.Navigation("AppointmentServices"); + }); + + modelBuilder.Entity("Domain.Entities.Service", b => + { + b.Navigation("AppointmentServices"); + }); + + modelBuilder.Entity("Domain.Entities.Tenant", b => + { + b.Navigation("Appointments"); + + b.Navigation("Services"); + + b.Navigation("Users"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.Navigation("Appointments"); + + b.Navigation("RefreshTokens"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Infrastructure/Migrations/20260519111054_NullableTenant.cs b/Infrastructure/Migrations/20260519111054_NullableTenant.cs new file mode 100644 index 0000000..2e933f8 --- /dev/null +++ b/Infrastructure/Migrations/20260519111054_NullableTenant.cs @@ -0,0 +1,37 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Infrastructure.Migrations +{ + /// + public partial class NullableTenant : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "TenantId", + table: "Users", + type: "uniqueidentifier", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uniqueidentifier"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "TenantId", + table: "Users", + type: "uniqueidentifier", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uniqueidentifier", + oldNullable: true); + } + } +} diff --git a/Infrastructure/Persistance/AppDbContext.cs b/Infrastructure/Persistence/AppDbContext.cs similarity index 71% rename from Infrastructure/Persistance/AppDbContext.cs rename to Infrastructure/Persistence/AppDbContext.cs index b35ff05..5d72d3b 100644 --- a/Infrastructure/Persistance/AppDbContext.cs +++ b/Infrastructure/Persistence/AppDbContext.cs @@ -1,11 +1,12 @@ using Domain.Entities; -using Infrastructure.Persistance.Configurations; +using Infrastructure.Persistence.Configurations; using Microsoft.EntityFrameworkCore; -namespace Infrastructure.Persistance; +namespace Infrastructure.Persistence; public class AppDbContext:DbContext { + public DbSet Tenants { get; set; } public DbSet Appointments =>Set(); public DbSet Services =>Set(); public DbSet Users =>Set(); @@ -18,6 +19,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppointmentConfiguration).Assembly); } + public Task SaveAsync(CancellationToken cancellationToken = new CancellationToken()) + { + return base.SaveChangesAsync(cancellationToken); + } + public AppDbContext(DbContextOptions options):base(options) { diff --git a/Infrastructure/Persistance/Configurations/AppointmentConfiguration.cs b/Infrastructure/Persistence/Configurations/AppointmentConfiguration.cs similarity index 81% rename from Infrastructure/Persistance/Configurations/AppointmentConfiguration.cs rename to Infrastructure/Persistence/Configurations/AppointmentConfiguration.cs index 432b077..91c4834 100644 --- a/Infrastructure/Persistance/Configurations/AppointmentConfiguration.cs +++ b/Infrastructure/Persistence/Configurations/AppointmentConfiguration.cs @@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Infrastructure.Persistance.Configurations; +namespace Infrastructure.Persistence.Configurations; public class AppointmentConfiguration:IEntityTypeConfiguration { @@ -34,6 +34,13 @@ public void Configure(EntityTypeBuilder builder) .WithOne(x=>x.Appointment) .HasForeignKey(x=>x.AppointmentId); + builder + .HasOne(ap=>ap.Tenant) + .WithMany(x=>x.Appointments) + .HasForeignKey(x=>x.TenantId) + .OnDelete(DeleteBehavior.Restrict); + + builder.HasCheckConstraint( "CK_Status_Valid_Values", "[Status] IN (0, 1,2)" diff --git a/Infrastructure/Persistance/Configurations/AppointmentServiceLinkConfiguration.cs b/Infrastructure/Persistence/Configurations/AppointmentServiceLinkConfiguration.cs similarity index 72% rename from Infrastructure/Persistance/Configurations/AppointmentServiceLinkConfiguration.cs rename to Infrastructure/Persistence/Configurations/AppointmentServiceLinkConfiguration.cs index 05adcc1..cf95912 100644 --- a/Infrastructure/Persistance/Configurations/AppointmentServiceLinkConfiguration.cs +++ b/Infrastructure/Persistence/Configurations/AppointmentServiceLinkConfiguration.cs @@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Infrastructure.Persistance.Configurations; +namespace Infrastructure.Persistence.Configurations; public class AppointmentServiceLinkConfiguration:IEntityTypeConfiguration { @@ -14,10 +14,12 @@ public void Configure(EntityTypeBuilder builder) builder.HasOne(x=>x.Appointment) .WithMany(x=>x.AppointmentServices) - .HasForeignKey(x=>x.AppointmentId); + .HasForeignKey(x=>x.AppointmentId) + .OnDelete(DeleteBehavior.Restrict); builder.HasOne(x => x.Service) .WithMany(x=>x.AppointmentServices) - .HasForeignKey(x=>x.ServiceId); + .HasForeignKey(x=>x.ServiceId) + .OnDelete(DeleteBehavior.Restrict); } } \ No newline at end of file diff --git a/Infrastructure/Persistance/Configurations/RefreshTokenConfiguration.cs b/Infrastructure/Persistence/Configurations/RefreshTokenConfiguration.cs similarity index 92% rename from Infrastructure/Persistance/Configurations/RefreshTokenConfiguration.cs rename to Infrastructure/Persistence/Configurations/RefreshTokenConfiguration.cs index 4c8000e..632b924 100644 --- a/Infrastructure/Persistance/Configurations/RefreshTokenConfiguration.cs +++ b/Infrastructure/Persistence/Configurations/RefreshTokenConfiguration.cs @@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Infrastructure.Persistance.Configurations; +namespace Infrastructure.Persistence.Configurations; public class RefreshTokenConfiguration:IEntityTypeConfiguration { diff --git a/Infrastructure/Persistance/Configurations/ServiceConfiguration.cs b/Infrastructure/Persistence/Configurations/ServiceConfiguration.cs similarity index 78% rename from Infrastructure/Persistance/Configurations/ServiceConfiguration.cs rename to Infrastructure/Persistence/Configurations/ServiceConfiguration.cs index 631ab74..57c5af5 100644 --- a/Infrastructure/Persistance/Configurations/ServiceConfiguration.cs +++ b/Infrastructure/Persistence/Configurations/ServiceConfiguration.cs @@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Infrastructure.Persistance.Configurations; +namespace Infrastructure.Persistence.Configurations; public class ServiceConfiguration:IEntityTypeConfiguration { @@ -24,5 +24,10 @@ public void Configure(EntityTypeBuilder builder) builder.HasMany(x=>x.AppointmentServices) .WithOne(x=>x.Service) .HasForeignKey(x=>x.ServiceId); + + builder + .HasOne(x=>x.Tenant) + .WithMany(x=>x.Services) + .HasForeignKey(x=>x.TenantId); } } \ No newline at end of file diff --git a/Infrastructure/Persistence/Configurations/TenantConfiguration.cs b/Infrastructure/Persistence/Configurations/TenantConfiguration.cs new file mode 100644 index 0000000..0748fea --- /dev/null +++ b/Infrastructure/Persistence/Configurations/TenantConfiguration.cs @@ -0,0 +1,34 @@ +using Domain.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Infrastructure.Persistence.Configurations; + +public class TenantConfiguration:IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Tenants"); + + builder.HasKey(x=>x.Id); + + builder + .Property(x => x.Name) + .HasMaxLength(100) + .IsRequired(); + + builder + .Property(p => p.DateJoined) + .IsRequired(); + + builder + .Property(p => p.ExpireSubscriptionDate) + .IsRequired(); + + builder + .Property(p=>p.Slug) + .HasMaxLength(200) + .IsRequired() + .IsUnicode(); + } +} \ No newline at end of file diff --git a/Infrastructure/Persistance/Configurations/UserConfiguration.cs b/Infrastructure/Persistence/Configurations/UserConfiguration.cs similarity index 85% rename from Infrastructure/Persistance/Configurations/UserConfiguration.cs rename to Infrastructure/Persistence/Configurations/UserConfiguration.cs index cda361c..0787d87 100644 --- a/Infrastructure/Persistance/Configurations/UserConfiguration.cs +++ b/Infrastructure/Persistence/Configurations/UserConfiguration.cs @@ -2,7 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; -namespace Infrastructure.Persistance.Configurations; +namespace Infrastructure.Persistence.Configurations; public class UserConfiguration:IEntityTypeConfiguration { @@ -42,7 +42,11 @@ public void Configure(EntityTypeBuilder builder) builder.Property(x => x.PasswordResetAttemptsCount) .IsRequired() .HasDefaultValue(0); - + builder + .HasOne(t => t.Tenant) + .WithMany(t=>t.Users) + .HasForeignKey(t => t.TenantId) + .OnDelete(DeleteBehavior.Cascade); builder.HasCheckConstraint( "CK_Role_Valid_Values", "[Role] IN (0, 1)" diff --git a/Infrastructure/Persistance/DbInitializer.cs b/Infrastructure/Persistence/DbInitializer.cs similarity index 95% rename from Infrastructure/Persistance/DbInitializer.cs rename to Infrastructure/Persistence/DbInitializer.cs index eb63cd1..6347ce2 100644 --- a/Infrastructure/Persistance/DbInitializer.cs +++ b/Infrastructure/Persistence/DbInitializer.cs @@ -3,7 +3,7 @@ using Domain.Enums; using Microsoft.Extensions.Configuration; -namespace Infrastructure.Persistance; +namespace Infrastructure.Persistence; public class DbInitializer { diff --git a/Infrastructure/Persistance/Migrations/20260209204358_InitialCleanSchema.Designer.cs b/Infrastructure/Persistence/Migrations/20260209204358_InitialCleanSchema.Designer.cs similarity index 99% rename from Infrastructure/Persistance/Migrations/20260209204358_InitialCleanSchema.Designer.cs rename to Infrastructure/Persistence/Migrations/20260209204358_InitialCleanSchema.Designer.cs index 29f15be..2d71931 100644 --- a/Infrastructure/Persistance/Migrations/20260209204358_InitialCleanSchema.Designer.cs +++ b/Infrastructure/Persistence/Migrations/20260209204358_InitialCleanSchema.Designer.cs @@ -1,6 +1,6 @@ // using System; -using Infrastructure.Persistance; +using Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; diff --git a/Infrastructure/Persistance/Migrations/20260209204358_InitialCleanSchema.cs b/Infrastructure/Persistence/Migrations/20260209204358_InitialCleanSchema.cs similarity index 100% rename from Infrastructure/Persistance/Migrations/20260209204358_InitialCleanSchema.cs rename to Infrastructure/Persistence/Migrations/20260209204358_InitialCleanSchema.cs diff --git a/Infrastructure/Persistance/Migrations/AppDbContextModelSnapshot.cs b/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs similarity index 72% rename from Infrastructure/Persistance/Migrations/AppDbContextModelSnapshot.cs rename to Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs index 98fbb2e..7c1f873 100644 --- a/Infrastructure/Persistance/Migrations/AppDbContextModelSnapshot.cs +++ b/Infrastructure/Persistence/Migrations/AppDbContextModelSnapshot.cs @@ -1,6 +1,6 @@ // using System; -using Infrastructure.Persistance; +using Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; @@ -37,6 +37,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Status") .HasColumnType("int"); + b.Property("TenantId") + .HasColumnType("uniqueidentifier"); + b.Property("Title") .IsRequired() .HasMaxLength(20) @@ -47,6 +50,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("AppointmentId"); + b.HasIndex("TenantId"); + b.HasIndex("UserId"); b.ToTable("Appointments", t => @@ -109,6 +114,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("DurationMinutes") .HasColumnType("int"); + b.Property("TenantId") + .HasColumnType("uniqueidentifier"); + b.Property("Title") .IsRequired() .HasMaxLength(20) @@ -116,9 +124,42 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasKey("Id"); + b.HasIndex("TenantId"); + b.ToTable("Service", (string)null); }); + modelBuilder.Entity("Domain.Entities.Tenant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DateJoined") + .HasColumnType("datetime2"); + + b.Property("ExpireSubscriptionDate") + .HasColumnType("datetime2"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Slug") + .IsRequired() + .HasMaxLength(200) + .IsUnicode(true) + .HasColumnType("nvarchar(200)"); + + b.Property("SubscriptionStatus") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("Tenants", (string)null); + }); + modelBuilder.Entity("Domain.Entities.User", b => { b.Property("Id") @@ -160,8 +201,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("Role") .HasColumnType("int"); + b.Property("TenantId") + .HasColumnType("uniqueidentifier"); + b.HasKey("Id"); + b.HasIndex("TenantId"); + b.ToTable("Users", null, t => { t.HasCheckConstraint("CK_Role_Valid_Values", "[Role] IN (0, 1)"); @@ -170,12 +216,20 @@ protected override void BuildModel(ModelBuilder modelBuilder) modelBuilder.Entity("Domain.Entities.Appointment", b => { + b.HasOne("Domain.Entities.Tenant", "Tenant") + .WithMany("Appointments") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + b.HasOne("Domain.Entities.User", "User") .WithMany("Appointments") .HasForeignKey("UserId") .OnDelete(DeleteBehavior.Cascade) .IsRequired(); + b.Navigation("Tenant"); + b.Navigation("User"); }); @@ -184,13 +238,13 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasOne("Domain.Entities.Appointment", "Appointment") .WithMany("AppointmentServices") .HasForeignKey("AppointmentId") - .OnDelete(DeleteBehavior.Cascade) + .OnDelete(DeleteBehavior.Restrict) .IsRequired(); b.HasOne("Domain.Entities.Service", "Service") .WithMany("AppointmentServices") .HasForeignKey("ServiceId") - .OnDelete(DeleteBehavior.Cascade) + .OnDelete(DeleteBehavior.Restrict) .IsRequired(); b.Navigation("Appointment"); @@ -209,6 +263,27 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Domain.Entities.Service", b => + { + b.HasOne("Domain.Entities.Tenant", "Tenant") + .WithMany("Services") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Tenant"); + }); + + modelBuilder.Entity("Domain.Entities.User", b => + { + b.HasOne("Domain.Entities.Tenant", "Tenant") + .WithMany("Users") + .HasForeignKey("TenantId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Tenant"); + }); + modelBuilder.Entity("Domain.Entities.Appointment", b => { b.Navigation("AppointmentServices"); @@ -219,6 +294,15 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("AppointmentServices"); }); + modelBuilder.Entity("Domain.Entities.Tenant", b => + { + b.Navigation("Appointments"); + + b.Navigation("Services"); + + b.Navigation("Users"); + }); + modelBuilder.Entity("Domain.Entities.User", b => { b.Navigation("Appointments"); diff --git a/Infrastructure/Persistance/Repositories/AppointmentRepository.cs b/Infrastructure/Persistence/Repositories/AppointmentRepository.cs similarity index 97% rename from Infrastructure/Persistance/Repositories/AppointmentRepository.cs rename to Infrastructure/Persistence/Repositories/AppointmentRepository.cs index dd771f4..532bf3a 100644 --- a/Infrastructure/Persistance/Repositories/AppointmentRepository.cs +++ b/Infrastructure/Persistence/Repositories/AppointmentRepository.cs @@ -3,7 +3,7 @@ using Domain.Entities; using Microsoft.EntityFrameworkCore; -namespace Infrastructure.Persistance.Repositories; +namespace Infrastructure.Persistence.Repositories; public class AppointmentRepository:IAppointmentRepository { diff --git a/Infrastructure/Persistance/Repositories/AppointmentServiceLinkRepository.cs b/Infrastructure/Persistence/Repositories/AppointmentServiceLinkRepository.cs similarity index 96% rename from Infrastructure/Persistance/Repositories/AppointmentServiceLinkRepository.cs rename to Infrastructure/Persistence/Repositories/AppointmentServiceLinkRepository.cs index 517d4ed..cde54a7 100644 --- a/Infrastructure/Persistance/Repositories/AppointmentServiceLinkRepository.cs +++ b/Infrastructure/Persistence/Repositories/AppointmentServiceLinkRepository.cs @@ -2,7 +2,7 @@ using Domain.Entities; using Microsoft.EntityFrameworkCore; -namespace Infrastructure.Persistance.Repositories; +namespace Infrastructure.Persistence.Repositories; public class AppointmentServiceLinkRepository:IAppointmenServiceLinkRepository { diff --git a/Infrastructure/Persistance/Repositories/RefreshTokenRepository.cs b/Infrastructure/Persistence/Repositories/RefreshTokenRepository.cs similarity index 95% rename from Infrastructure/Persistance/Repositories/RefreshTokenRepository.cs rename to Infrastructure/Persistence/Repositories/RefreshTokenRepository.cs index 2045206..5cc725e 100644 --- a/Infrastructure/Persistance/Repositories/RefreshTokenRepository.cs +++ b/Infrastructure/Persistence/Repositories/RefreshTokenRepository.cs @@ -2,7 +2,7 @@ using Domain.Entities; using Microsoft.EntityFrameworkCore; -namespace Infrastructure.Persistance.Repositories; +namespace Infrastructure.Persistence.Repositories; public class RefreshTokenRepository:IRefreshTokenRepository { diff --git a/Infrastructure/Persistance/Repositories/ServiceRepository.cs b/Infrastructure/Persistence/Repositories/ServiceRepository.cs similarity index 96% rename from Infrastructure/Persistance/Repositories/ServiceRepository.cs rename to Infrastructure/Persistence/Repositories/ServiceRepository.cs index 36191b9..64f3219 100644 --- a/Infrastructure/Persistance/Repositories/ServiceRepository.cs +++ b/Infrastructure/Persistence/Repositories/ServiceRepository.cs @@ -3,7 +3,7 @@ using Domain.Entities; using Microsoft.EntityFrameworkCore; -namespace Infrastructure.Persistance.Repositories; +namespace Infrastructure.Persistence.Repositories; public class ServiceRepository:IServiceRepository { diff --git a/Infrastructure/Persistence/Repositories/TenantRepository.cs b/Infrastructure/Persistence/Repositories/TenantRepository.cs new file mode 100644 index 0000000..babe646 --- /dev/null +++ b/Infrastructure/Persistence/Repositories/TenantRepository.cs @@ -0,0 +1,26 @@ +using Application.Features.Tenant.Interfaces; +using Domain.Entities; +using Microsoft.Extensions.Logging; + +namespace Infrastructure.Persistence.Repositories; + +public class TenantRepository:TenantRepositoryContract +{ + private readonly AppDbContext _context; + + public TenantRepository(AppDbContext context) + { + _context = context; + } + + public async Task RegisterTenant(Tenant tenant) + { + await _context + .AddAsync(tenant); + } + public async Task SaveAsync() + { + await _context + .SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/Infrastructure/Persistance/Repositories/UserRepository.cs b/Infrastructure/Persistence/Repositories/UserRepository.cs similarity index 96% rename from Infrastructure/Persistance/Repositories/UserRepository.cs rename to Infrastructure/Persistence/Repositories/UserRepository.cs index faa8485..0b13bad 100644 --- a/Infrastructure/Persistance/Repositories/UserRepository.cs +++ b/Infrastructure/Persistence/Repositories/UserRepository.cs @@ -3,7 +3,7 @@ using Domain.Entities; using Microsoft.EntityFrameworkCore; -namespace Infrastructure.Persistance.Repositories; +namespace Infrastructure.Persistence.Repositories; public class UserRepository : IUserRepository { diff --git a/api/Controllers/TenantController.cs b/api/Controllers/TenantController.cs new file mode 100644 index 0000000..94200cb --- /dev/null +++ b/api/Controllers/TenantController.cs @@ -0,0 +1,24 @@ +using Application.Features.Tenant.DTO_s; +using Application.Features.Tenant.Services; +using Microsoft.AspNetCore.Mvc; + +namespace api.Controllers; +[ApiController] +[Route("api/[controller]")] +public class TenantController : ControllerBase +{ + private readonly TenantServiceContract _tenantService; + + public TenantController(TenantServiceContract tenantService) + { + _tenantService = tenantService; + } + + [HttpPost] + public async Task RegisterTenant([FromBody] RegisterTenantDTO dto) + { + // Todo: expire date base on plan + var result=await _tenantService.Register(dto); + return Ok(result); + } +} \ No newline at end of file diff --git a/api/Program.cs b/api/Program.cs index a9c0357..29541b4 100644 --- a/api/Program.cs +++ b/api/Program.cs @@ -6,7 +6,7 @@ using FluentValidation; using FluentValidation.AspNetCore; using Infrastructure; -using Infrastructure.Persistance; +using Infrastructure.Persistence; using Infrastructure.Security.Hashing; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Mvc;