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/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/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;