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/Application/Application.csproj b/Application/Application.csproj
index 990d47f..a79ac23 100644
--- a/Application/Application.csproj
+++ b/Application/Application.csproj
@@ -18,4 +18,9 @@
+
+
+
+
+
diff --git a/Application/Common/Interfaces/IJwtTokenGenerator.cs b/Application/Common/Interfaces/IJwtTokenGenerator.cs
index d6fca23..6cebaf0 100644
--- a/Application/Common/Interfaces/IJwtTokenGenerator.cs
+++ b/Application/Common/Interfaces/IJwtTokenGenerator.cs
@@ -1,3 +1,4 @@
+using Domain.Aggregates.User;
using Domain.Entities;
namespace Application.Common.Interfaces;
diff --git a/Application/Common/Interfaces/IUSerContext.cs b/Application/Common/Interfaces/IUserContext.cs
similarity index 85%
rename from Application/Common/Interfaces/IUSerContext.cs
rename to Application/Common/Interfaces/IUserContext.cs
index 50ab73e..3c67a1e 100644
--- a/Application/Common/Interfaces/IUSerContext.cs
+++ b/Application/Common/Interfaces/IUserContext.cs
@@ -2,7 +2,7 @@
namespace Application.Common.Interfaces;
-public interface IUSerContext
+public interface IUserContext
{
Guid UserId { get;}
string? Email { get;}
diff --git a/Application/Features/Appointments/Interfaces/IAppointmentRepository.cs b/Application/Features/Appointments/Interfaces/IAppointmentRepository.cs
deleted file mode 100644
index 9fb57b7..0000000
--- a/Application/Features/Appointments/Interfaces/IAppointmentRepository.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using Application.Features.Appointments.DTOs;
-using Domain.Entities;
-
-namespace Application.Features.Appointments.Interfaces;
-
-public interface IAppointmentRepository
-{
- Task AddAsync(Appointment appointment);
- Task SaveAsync();
- Task> ViewAppointments();
- Task> ViewAppointments(Guid userId);
- Task GetAppointmentByIdAsync(Guid appointmentId);
- Task IsExistBy(Guid appointmentId);
-}
\ No newline at end of file
diff --git a/Application/Features/Appointments/Interfaces/IAppointmentService.cs b/Application/Features/Appointments/Interfaces/IAppointmentService.cs
index 3fa756f..521acb0 100644
--- a/Application/Features/Appointments/Interfaces/IAppointmentService.cs
+++ b/Application/Features/Appointments/Interfaces/IAppointmentService.cs
@@ -1,4 +1,5 @@
using Application.Features.Appointments.DTOs;
+using Domain.Aggregates.Appointment;
using Domain.Entities;
namespace Application.Features.Appointments.Interfaces;
diff --git a/Application/Features/Appointments/Services/AppointmentService.cs b/Application/Features/Appointments/Services/AppointmentService.cs
index 9501aef..2a50023 100644
--- a/Application/Features/Appointments/Services/AppointmentService.cs
+++ b/Application/Features/Appointments/Services/AppointmentService.cs
@@ -1,12 +1,12 @@
using Application.Common.Interfaces;
using Application.Features.Appointments.DTOs;
using Application.Features.Appointments.Interfaces;
-using Application.Features.AppointmentServiceLink.Interfaces;
using Application.Features.Auth.Interfaces;
-using Application.Features.Service.Interfaces;
+using Domain.Aggregates.Appointment;
using Domain.Entities;
using Domain.Enums;
using Domain.Exceptions;
+using Domain.Repository;
namespace Application.Features.Appointments.Services;
@@ -16,11 +16,11 @@ public class AppointmentService : IAppointmentService
private readonly IUserRepository _userRepository;
private readonly IServiceRepository _serviceRepository;
private readonly IAppointmenServiceLinkRepository _appointmenServiceLinkRepository;
- private readonly IUSerContext _userContext;
+ private readonly IUserContext _userContext;
public AppointmentService(IAppointmentRepository repository, IUserRepository userRepository,
IServiceRepository serviceRepository, IAppointmenServiceLinkRepository appointmenServiceLinkRepository,
- IUSerContext userContext)
+ IUserContext userContext)
{
_appointmentRepository = repository;
_userRepository = userRepository;
diff --git a/Application/Features/Auth/Interfaces/IUserRepository.cs b/Application/Features/Auth/Interfaces/IUserRepository.cs
deleted file mode 100644
index 37a16a5..0000000
--- a/Application/Features/Auth/Interfaces/IUserRepository.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-using Application.Features.Auth.DTOs;
-using Domain.Entities;
-
-namespace Application.Features.Auth.Interfaces;
-
-public interface IUserRepository
-{
- Task RegisterUserAsync(User user);
- Task GetUserByEmailAsync(string email);
- Task IsUserExistsByIdAsync(Guid userId);
- Task SaveChangesAsync();
- Task> GetAllUsersAsync();
- Task GetUserByIdAsync(Guid userId);
-}
\ No newline at end of file
diff --git a/Application/Features/Auth/Services/PasswordRecoveryService.cs b/Application/Features/Auth/Services/PasswordRecoveryService.cs
index 6127357..3b5e442 100644
--- a/Application/Features/Auth/Services/PasswordRecoveryService.cs
+++ b/Application/Features/Auth/Services/PasswordRecoveryService.cs
@@ -2,6 +2,7 @@
using Application.Features.Auth.Interfaces;
using Domain.Entities;
using Domain.Exceptions;
+using Domain.Repository;
namespace Application.Features.Auth.Services;
@@ -55,13 +56,5 @@ public async Task ForgetPasswordAsync(string email)
);
await _userRepository.SaveChangesAsync();
-
- //Implement Email Service
-
- // await _emailService.SendAsync(
- // email,
- // "Password Reset Code",
- // $"Your verification code is: {code}"
- // );
}
}
\ No newline at end of file
diff --git a/Application/Features/Auth/Services/UserService.cs b/Application/Features/Auth/Services/UserService.cs
index ccdcca1..8c8ad0f 100644
--- a/Application/Features/Auth/Services/UserService.cs
+++ b/Application/Features/Auth/Services/UserService.cs
@@ -3,7 +3,9 @@
using Application.Common.Interfaces.Repositories;
using Application.Features.Auth.DTOs;
using Application.Features.Auth.Interfaces;
+using Domain.Aggregates.User;
using Domain.Entities;
+using Domain.Repository;
namespace Application.Features.Auth.Services;
@@ -14,12 +16,12 @@ public class UserService : IUserService
private readonly IPasswordHasher _passwordHasher;
private readonly IJwtTokenService _jwtTokenService;
private readonly IRefreshTokenRepository _refreshTokenRepository;
- private readonly IUSerContext _userContext;
+ private readonly IUserContext _userContext;
private readonly IVerificationCodeGenerator _verificationCodeGenerator;
public UserService(IUserRepository repository, IJwtTokenService jwtTokenGenerator,
IRefreshTokenRepository refreshTokenRepository, IHasher hasher, IPasswordHasher passwordHasher,
- IUSerContext userContext, IVerificationCodeGenerator verificationCodeGenerator)
+ IUserContext userContext, IVerificationCodeGenerator verificationCodeGenerator)
{
_userRepository = repository;
_jwtTokenService = jwtTokenGenerator;
diff --git a/Application/Features/Service/Interfaces/IServiceRepository.cs b/Application/Features/Service/Interfaces/IServiceRepository.cs
deleted file mode 100644
index 379feef..0000000
--- a/Application/Features/Service/Interfaces/IServiceRepository.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using Application.Features.Service.DTOs;
-
-namespace Application.Features.Service.Interfaces;
-
-public interface IServiceRepository
-{
- Task CreatServiceAsync(Domain.Entities.Service service);
- Task SaveChangesAsync();
- Task DeleteServiceAsync(Guid serviceId);
- Task> ViewAllServiceAsync();
- Task GetServiceByIdAsync(Guid serviceId);
- Task> GetServiceListByIdsAsync(List serviceIds);
-}
\ No newline at end of file
diff --git a/Application/Features/Service/Services/ServiceAppService.cs b/Application/Features/Service/Services/ServiceAppService.cs
index d9fbc74..7c2dbca 100644
--- a/Application/Features/Service/Services/ServiceAppService.cs
+++ b/Application/Features/Service/Services/ServiceAppService.cs
@@ -1,16 +1,16 @@
using Application.Common.Interfaces;
using Application.Features.Service.DTOs;
-using Application.Features.Service.Interfaces;
using Domain.Enums;
using Domain.Exceptions;
+using Domain.Repository;
namespace Application.Features.Service.Services;
public class ServiceAppService:IServiceAppService
{
private readonly IServiceRepository _repository;
- private readonly IUSerContext _userContext;
- public ServiceAppService(IServiceRepository repository, IUSerContext context)
+ private readonly IUserContext _userContext;
+ public ServiceAppService(IServiceRepository repository, IUserContext context)
{
_repository = repository;
_userContext = context;
@@ -24,7 +24,7 @@ private void EnsureAdmin()
public async Task CreateServiceAsync(CreateService createService)
{
EnsureAdmin();
- var service=new Domain.Entities.Service(createService.Title,createService.DurationMinutes);
+ var service=new Domain.Aggregates.Service.Service(createService.Title,createService.DurationMinutes);
await _repository.CreatServiceAsync(service);
return $"{service.Title} created";
}
diff --git a/Domain/Aggregates/Appointment/Appointment.cs b/Domain/Aggregates/Appointment/Appointment.cs
new file mode 100644
index 0000000..398beea
--- /dev/null
+++ b/Domain/Aggregates/Appointment/Appointment.cs
@@ -0,0 +1,64 @@
+using Domain.Base;
+using Domain.Enums;
+using Domain.ValueObject;
+
+namespace Domain.Aggregates.Appointment
+{
+ public class Appointment:AggregatedRoot
+ {
+ public Guid AppointmentId { get; private set; }
+ public Guid UserId { get; private set; }
+ public string Title { get; private set; }
+ public AppointmentTimeRange TimeRange { get; private set; }
+ public AppointmentStatus Status { get; private set; }
+
+ private readonly List _serviceIds = new();
+ public IReadOnlyList ServiceIds=>_serviceIds;
+
+ public void AddServices(IEnumerable serviceIds)
+ {
+ foreach (var id in serviceIds.Distinct())
+ {
+ if (!_serviceIds.Contains(id))
+ _serviceIds.Add(id);
+ }
+ }
+
+ private Appointment() { } // For EF
+
+ public Appointment(Guid userId,AppointmentTimeRange timeRange, string title)
+ {
+ if (string.IsNullOrEmpty(title))
+ throw new ArgumentException("Title cannot be null or empty");
+ AppointmentId = Guid.NewGuid();
+ UserId = userId;
+ TimeRange = timeRange??throw new ArgumentNullException(nameof(timeRange)) ;
+ Title = title;
+ Status = AppointmentStatus.Reserved;
+ }
+
+ public void Cancel()
+ {
+ if (Status == AppointmentStatus.Cancelled)
+ throw new InvalidOperationException("Already cancelled");
+
+ Status = AppointmentStatus.Cancelled;
+ }
+
+ public void Reschedule(AppointmentTimeRange timeRange)
+ {
+ if (Status == AppointmentStatus.Cancelled)
+ throw new InvalidOperationException("Cannot reschedule cancelled appointment");
+ TimeRange = timeRange ?? throw new ArgumentNullException(nameof(timeRange));
+ }
+ public void ChangeTitle(string title)
+ {
+ if (Status == AppointmentStatus.Cancelled)
+ throw new InvalidOperationException("Cannot edit cancelled appointment");
+
+ if (string.IsNullOrEmpty(title))
+ throw new ArgumentException("Title cannot be null or empty");
+ Title=title;
+ }
+ }
+}
diff --git a/Domain/Aggregates/Service/Service.cs b/Domain/Aggregates/Service/Service.cs
new file mode 100644
index 0000000..6e15af6
--- /dev/null
+++ b/Domain/Aggregates/Service/Service.cs
@@ -0,0 +1,32 @@
+using Domain.Base;
+
+namespace Domain.Aggregates.Service;
+
+public class Service:AggregatedRoot
+{
+ public Guid Id { get; private set; }
+ public string Title { get; private set; }
+ public TimeSpan DurationMinutes { get; private set; }
+ private Service() { }
+
+ public Service(string title, TimeSpan durationMinutes)
+ {
+ if (string.IsNullOrEmpty(title))
+ throw new ArgumentException("Title required");
+ if (durationMinutes.TotalMinutes <= 0)
+ throw new ArgumentException("Invalid duration");
+ Id = Guid.NewGuid();
+ Title = title;
+ DurationMinutes = durationMinutes;
+ }
+
+ public void Edit(TimeSpan durationMinutes,string title)
+ {
+ if (string.IsNullOrEmpty(title))
+ throw new ArgumentException("Title required");
+ if (durationMinutes.TotalMinutes <= 0)
+ throw new ArgumentException("Invalid duration");
+ DurationMinutes = durationMinutes;
+ Title = title;
+ }
+}
\ No newline at end of file
diff --git a/Domain/Aggregates/User/User.cs b/Domain/Aggregates/User/User.cs
new file mode 100644
index 0000000..69cf40a
--- /dev/null
+++ b/Domain/Aggregates/User/User.cs
@@ -0,0 +1,95 @@
+using Domain.Base;
+using Domain.Entities;
+using Domain.Enums;
+using Domain.ValueObject;
+
+namespace Domain.Aggregates.User;
+
+public class User:AggregatedRoot
+{
+ public Guid Id { get; private set; }
+ public string FullName { get; private set; }
+ public UserRole Role { get; private set; }
+ public Email Email { get; private set; }
+ public PhoneNumber PhoneNumber { get; private set; }
+ public Password Password { get; private set; }
+ public string? PasswordResetCodeHash { get; private set; }
+ public DateTime? PasswordResetCodeExpireAt { get; private set; }
+ public int PasswordResetAttemptsCount { get; private set; }
+ private readonly List _refreshTokens = new();
+ public IReadOnlyList RefreshTokens => _refreshTokens;
+
+ public void AddRefreshToken(string refreshToken, DateTime expiresAt)
+ {
+ _refreshTokens.RemoveAll(tk => tk.IsExpired);
+ _refreshTokens.Add(new RefreshToken(refreshToken, expiresAt));
+ }
+
+ public void RevokeRefreshToken(Guid tokenId)
+ {
+ var token = _refreshTokens.FirstOrDefault(tk => tk.Id == tokenId);
+ if (token == null) throw new InvalidOperationException("Refresh token not found");
+ token.Revoke();
+ }
+
+ private User()
+ {
+ }
+
+ public User(string fullName, Email email, PhoneNumber phoneNumber, Password password)
+ {
+ Id = Guid.NewGuid();
+ FullName = fullName;
+ Role = UserRole.User;
+ Email = email;
+ PhoneNumber = phoneNumber;
+ Password = password;
+ }
+
+ public User(string fullName, Email email, PhoneNumber phoneNumber, UserRole role, Password password)
+ {
+ Id = Guid.NewGuid();
+ FullName = fullName;
+ Role = role;
+ Email = email ?? throw new ArgumentNullException(nameof(email));
+ PhoneNumber = phoneNumber ?? throw new ArgumentNullException(nameof(phoneNumber));
+ Password = password ?? throw new ArgumentNullException(nameof(password));
+ }
+
+ public void UpdateProfile(string fullName, PhoneNumber phoneNumber)
+ {
+ if (string.IsNullOrWhiteSpace(fullName)) throw new ArgumentException("Full name required");
+ if (phoneNumber == null) throw new ArgumentException("Phone number required");
+ FullName = fullName;
+ PhoneNumber = phoneNumber;
+ }
+
+ public void ResetPassword(string codeHash, DateTime expiresAt)
+ {
+ if (string.IsNullOrWhiteSpace(codeHash)) throw new ArgumentException("Code hash required");
+ if (expiresAt <= DateTime.UtcNow) throw new ArgumentException("Expires must be in future");
+ PasswordResetCodeHash = codeHash;
+ PasswordResetCodeExpireAt = expiresAt;
+ PasswordResetAttemptsCount = 0;
+ }
+
+ private const int MaxResetAttempts = 5;
+
+ public bool CanUseResetPassword(string codeHash, DateTime now)
+ {
+ if (PasswordResetCodeExpireAt == null || now > PasswordResetCodeExpireAt)
+ return false;
+ if (PasswordResetAttemptsCount >= MaxResetAttempts)
+ return false;
+ return PasswordResetCodeHash == codeHash;
+ }
+
+ public void IncreasePasswordResetAttemptCount() => PasswordResetAttemptsCount++;
+
+ public void ClearPasswordResetCode()
+ {
+ PasswordResetCodeHash = null;
+ PasswordResetCodeExpireAt = null;
+ PasswordResetAttemptsCount = 0;
+ }
+}
\ No newline at end of file
diff --git a/Domain/Base/AggregatedRoot.cs b/Domain/Base/AggregatedRoot.cs
new file mode 100644
index 0000000..3ab8ce9
--- /dev/null
+++ b/Domain/Base/AggregatedRoot.cs
@@ -0,0 +1,11 @@
+namespace Domain.Base;
+
+public class AggregatedRoot:EntityBase
+{
+ private readonly List _domainEvents=[];
+ public IReadOnlyCollection DomainEvents => _domainEvents;
+
+ protected void AddDomainEvent(DomainEvent domainEvent)=> _domainEvents.Add(domainEvent);
+
+ public void ClearDomainEvents() => _domainEvents.Clear();
+}
\ No newline at end of file
diff --git a/Domain/Base/DomainEvent.cs b/Domain/Base/DomainEvent.cs
new file mode 100644
index 0000000..0d3dc1e
--- /dev/null
+++ b/Domain/Base/DomainEvent.cs
@@ -0,0 +1,6 @@
+namespace Domain.Base;
+
+public abstract record DomainEvent
+{
+ public DateTime OccurredOn { get; init; } = DateTime.UtcNow;
+}
\ No newline at end of file
diff --git a/Domain/Base/EntityBase.cs b/Domain/Base/EntityBase.cs
new file mode 100644
index 0000000..e73f003
--- /dev/null
+++ b/Domain/Base/EntityBase.cs
@@ -0,0 +1,6 @@
+namespace Domain.Base;
+
+public class EntityBase
+{
+ public Guid Id { get; protected set; }
+}
\ No newline at end of file
diff --git a/Domain/Entities/Appointment.cs b/Domain/Entities/Appointment.cs
deleted file mode 100644
index f2852e1..0000000
--- a/Domain/Entities/Appointment.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-using Domain.Enums;
-
-namespace Domain.Entities
-{
- public class Appointment
- {
- public Guid AppointmentId { get; private set; }
- public Guid UserId { get; private set; }
- public User User { get; private set; } = null!;
- public string Title { get; private set; }
- public DateTime StartTime { get; private set; }
- public DateTime EndTime { get; private set; }
- public AppointmentStatus Status { get; private set; }
-
- public ICollection AppointmentServices { get; private set; } = new List();
-
- private Appointment() { } // For EF
-
- public Appointment(Guid userId, DateTime startTime, DateTime endTime, string title)
- {
- if (startTime == default || endTime == default)
- throw new ArgumentException("StartTime and EndTime are required");
- if (endTime <= startTime)
- throw new ArgumentException("EndTime must be after StartTime");
-
- AppointmentId = Guid.NewGuid();
- UserId = userId;
- StartTime = startTime;
- EndTime = endTime;
- Title = title;
- Status = AppointmentStatus.Reserved;
- }
-
- public void AddServices(IEnumerable services)
- {
- foreach (var service in services)
- {
- AppointmentServices.Add(new AppointmentServiceLink(AppointmentId, service.Id));
- }
- }
-
- public void Cancel()
- {
- if (Status == AppointmentStatus.Cancelled)
- throw new InvalidOperationException("Already cancelled");
-
- Status = AppointmentStatus.Cancelled;
- }
-
- public void Edit(DateTime startTime, DateTime endTime)
- {
- if (startTime == default || endTime == default)
- throw new ArgumentException("StartTime and EndTime are required");
- if (endTime <= startTime)
- throw new ArgumentException("EndTime must be after StartTime");
-
- StartTime = startTime;
- EndTime = endTime;
- }
- }
-}
diff --git a/Domain/Entities/AppointmentServiceLink.cs b/Domain/Entities/AppointmentServiceLink.cs
deleted file mode 100644
index df95475..0000000
--- a/Domain/Entities/AppointmentServiceLink.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-namespace Domain.Entities;
-
-public class AppointmentServiceLink
-{
- public Guid AppointmentId { get; set; }
- public Appointment Appointment { get; set; }
- public Guid ServiceId { get; set; }
- public Service Service { get; set; }
-
- private AppointmentServiceLink() { }
-
- public AppointmentServiceLink(Guid appointmentId, Guid serviceId)
- {
- AppointmentId = appointmentId;
- ServiceId = serviceId;
- }
-}
\ No newline at end of file
diff --git a/Domain/Entities/Refreshtoken.cs b/Domain/Entities/Refreshtoken.cs
index d198a14..6e712a3 100644
--- a/Domain/Entities/Refreshtoken.cs
+++ b/Domain/Entities/Refreshtoken.cs
@@ -2,12 +2,20 @@ namespace Domain.Entities;
public class RefreshToken
{
- public Guid Id { get; set; }
+ public Guid Id { get; private set; }
- public string Token { get; set; } = null!;
- public DateTime ExpiresAt { get; set; }
- public bool IsRevoked { get; set; }
+ public string Token { get; private set; } = null!;
+ public DateTime ExpiresAt { get; private set; }
+ public bool IsRevoked { get; private set; }
+ public bool IsExpired => ExpiresAt < DateTime.UtcNow;
+ public bool IsActive => !IsExpired && !IsRevoked;
- public Guid UserId { get; set; }
- public User User { get; set; } = null!;
-}
+ internal RefreshToken(string token, DateTime expiresAt)
+ {
+ Id = Guid.NewGuid();
+ Token = token ?? throw new ArgumentNullException(nameof(token));
+ ExpiresAt = expiresAt;
+ IsRevoked = false;
+ }
+ public void Revoke()=> IsRevoked = true;
+}
\ No newline at end of file
diff --git a/Domain/Entities/Service.cs b/Domain/Entities/Service.cs
deleted file mode 100644
index 9aca70f..0000000
--- a/Domain/Entities/Service.cs
+++ /dev/null
@@ -1,27 +0,0 @@
-namespace Domain.Entities;
-
-public class Service
-{
- public Guid Id { get; private set; }
- public string Title { get; private set; }
- public int DurationMinutes { get; private set; }
- public ICollection AppointmentServices { get; set; }=new List();
-
- private Service() { }
-
- public Service(string title, int durationMinutes)
- {
- if (durationMinutes <= 0)
- throw new ArgumentException("Invalid duration");
-
- Id = Guid.NewGuid();
- Title = title;
- DurationMinutes = durationMinutes;
- }
-
- public void Edit(int durationMinutes,string title)
- {
- DurationMinutes = durationMinutes;
- Title = title;
- }
-}
\ No newline at end of file
diff --git a/Domain/Entities/User.cs b/Domain/Entities/User.cs
deleted file mode 100644
index 70602ec..0000000
--- a/Domain/Entities/User.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-using Domain.Enums;
-
-namespace Domain.Entities;
-
-public class User
-{
- public Guid Id { get; private set; }
- public string FullName { get; private set; }
- public UserRole Role { get; private set; }
- public string Email { get; private set; }
- public string PhoneNumber { get; private set; }
- public string Password { get; private set; }
- public string? PasswordResetCodeHash { get; private set; }
- public DateTime? PasswordResetCodeExpireAt { get; private set; }
- public int PasswordResetAttemptsCount { get; private set; }
- public ICollection RefreshTokens { get; private set; } = new List();
- public ICollection Appointments { get; private set; } = new List();
-
- public User()
- {
- }
-
- public User(string fullName, string email, string phoneNumber, string password)
- {
- Id = Guid.NewGuid();
- FullName = fullName;
- Role = UserRole.User;
- Email = email;
- PhoneNumber = phoneNumber;
- Password = password;
- }
-
- public User(string fullName, string email, string phoneNumber, UserRole role, string password)
- {
- Id = Guid.NewGuid();
- FullName = fullName;
- Role = role;
- Email = email;
- PhoneNumber = phoneNumber;
- Password = password;
- }
-
- public void UpdateProfile(string fullName, string phoneNumber)
- {
- FullName = fullName;
- PhoneNumber = phoneNumber;
- }
-
- public void ResetPassword(string codeHash, DateTime expiresAt)
- {
- PasswordResetCodeHash = codeHash;
- PasswordResetCodeExpireAt = expiresAt;
- PasswordResetAttemptsCount = 0;
- }
-
- public bool CanUseResetPassword(string codeHash, DateTime now)
- {
- if (PasswordResetCodeExpireAt == null || now > PasswordResetCodeExpireAt)
- return false;
-
- return PasswordResetCodeHash == codeHash;
- }
- public void IncreasePasswordResetAttemptCount()=>PasswordResetAttemptsCount++;
-
- public void ClearPasswordResetCode()
- {
- PasswordResetCodeHash = null;
- PasswordResetCodeExpireAt = null;
- PasswordResetAttemptsCount = 0;
- }
-}
\ No newline at end of file
diff --git a/Domain/Events/Appointment/AppointmentCacelledEvent.cs b/Domain/Events/Appointment/AppointmentCacelledEvent.cs
new file mode 100644
index 0000000..2d6f696
--- /dev/null
+++ b/Domain/Events/Appointment/AppointmentCacelledEvent.cs
@@ -0,0 +1,8 @@
+using Domain.Base;
+
+namespace Domain.Events.Appointment;
+
+public record AppointmentCancelledEvent(
+ Guid AppointmentId,
+ Guid ServiceId,
+ Guid UserId) : DomainEvent;
\ No newline at end of file
diff --git a/Domain/Events/Appointment/AppointmentCompletedEvent.cs b/Domain/Events/Appointment/AppointmentCompletedEvent.cs
new file mode 100644
index 0000000..57bc879
--- /dev/null
+++ b/Domain/Events/Appointment/AppointmentCompletedEvent.cs
@@ -0,0 +1,8 @@
+using Domain.Base;
+
+namespace Domain.Events.Appointment;
+
+public record AppointmentCompletedEvent(
+ Guid AppointmentId,
+ Guid ServiceId,
+ Guid UserId) : DomainEvent;
\ No newline at end of file
diff --git a/Domain/Events/Appointment/AppointmentCreatedEvent.cs b/Domain/Events/Appointment/AppointmentCreatedEvent.cs
new file mode 100644
index 0000000..f21087d
--- /dev/null
+++ b/Domain/Events/Appointment/AppointmentCreatedEvent.cs
@@ -0,0 +1,8 @@
+using Domain.Base;
+
+namespace Domain.Events.Appointment;
+
+public record AppointmentCreatedEvent(
+ Guid AppointmentId,
+ Guid ServiceId,
+ Guid UserId) : DomainEvent;
\ No newline at end of file
diff --git a/Domain/Events/Appointment/AppointmentRescheduledEvent.cs b/Domain/Events/Appointment/AppointmentRescheduledEvent.cs
new file mode 100644
index 0000000..1f58df7
--- /dev/null
+++ b/Domain/Events/Appointment/AppointmentRescheduledEvent.cs
@@ -0,0 +1,8 @@
+using Domain.Base;
+
+namespace Domain.Events.Appointment;
+
+public record AppointmentRescheduledEvent(
+ Guid AppointmentId,
+ Guid ServiceId,
+ Guid UserId) : DomainEvent;
\ No newline at end of file
diff --git a/Domain/Events/Service/ServiceCreatedEvent.cs b/Domain/Events/Service/ServiceCreatedEvent.cs
new file mode 100644
index 0000000..be1f69d
--- /dev/null
+++ b/Domain/Events/Service/ServiceCreatedEvent.cs
@@ -0,0 +1,8 @@
+using Domain.Base;
+
+namespace Domain.Events.Service;
+
+public record ServiceCreatedEvent(
+ Guid ServiceId,
+ string ServiceName,
+ TimeSpan DurationTime):DomainEvent;
\ No newline at end of file
diff --git a/Domain/Events/Service/ServiceUpdatedEvent.cs b/Domain/Events/Service/ServiceUpdatedEvent.cs
new file mode 100644
index 0000000..e40b1a4
--- /dev/null
+++ b/Domain/Events/Service/ServiceUpdatedEvent.cs
@@ -0,0 +1,8 @@
+using Domain.Base;
+
+namespace Domain.Events.Service;
+
+public record ServiceUpdatedEvent(
+ Guid ServiceId,
+ string ServiceName,
+ TimeSpan DurationTime):DomainEvent;
\ No newline at end of file
diff --git a/Domain/Events/User/UserProfileUpdatedEvent.cs b/Domain/Events/User/UserProfileUpdatedEvent.cs
new file mode 100644
index 0000000..aab158e
--- /dev/null
+++ b/Domain/Events/User/UserProfileUpdatedEvent.cs
@@ -0,0 +1,7 @@
+using Domain.Base;
+
+namespace Domain.Events.User;
+
+public record UserProfileUpdatedEvent(
+ Guid UserId,
+ string NewFullName):DomainEvent;
\ No newline at end of file
diff --git a/Domain/Events/User/UserRegisteredEvent.cs b/Domain/Events/User/UserRegisteredEvent.cs
new file mode 100644
index 0000000..feaf741
--- /dev/null
+++ b/Domain/Events/User/UserRegisteredEvent.cs
@@ -0,0 +1,7 @@
+using Domain.Base;
+
+namespace Domain.Events.User;
+
+public record UserRegisteredEvent(
+ Guid UserId,
+ string Email):DomainEvent;
\ No newline at end of file
diff --git a/Domain/Events/User/UserRemovedEvent.cs b/Domain/Events/User/UserRemovedEvent.cs
new file mode 100644
index 0000000..abc933b
--- /dev/null
+++ b/Domain/Events/User/UserRemovedEvent.cs
@@ -0,0 +1,6 @@
+using Domain.Base;
+
+namespace Domain.Events.User;
+
+public record UserRemovedEvent(
+ Guid UserId):DomainEvent;
\ No newline at end of file
diff --git a/Domain/Repository/IAppointmentRepository.cs b/Domain/Repository/IAppointmentRepository.cs
new file mode 100644
index 0000000..5cd28b7
--- /dev/null
+++ b/Domain/Repository/IAppointmentRepository.cs
@@ -0,0 +1,12 @@
+using Domain.Aggregates.Appointment;
+
+namespace Domain.Repository;
+
+public interface IAppointmentRepository
+{
+ Task AddAppointmentAsync(Appointment appointment);
+ Task> ViewAppointments();
+ Task> ViewAppointments(Guid userId);
+ Task GetAppointmentByIdAsync(Guid appointmentId);
+ Task IsExistBy(Guid appointmentId);
+}
\ No newline at end of file
diff --git a/Domain/Repository/IServiceRepository.cs b/Domain/Repository/IServiceRepository.cs
new file mode 100644
index 0000000..37de629
--- /dev/null
+++ b/Domain/Repository/IServiceRepository.cs
@@ -0,0 +1,12 @@
+using Domain.Aggregates.Service;
+
+namespace Domain.Repository;
+
+public interface IServiceRepository
+{
+ Task AddServiceAsync(Service service);
+ Task DeleteServiceAsync(Guid serviceId);
+ Task> ViewAllServiceAsync();
+ Task GetServiceByIdAsync(Guid serviceId);
+ Task> GetServiceListByIdsAsync(List serviceIds);
+}
\ No newline at end of file
diff --git a/Domain/Repository/IUserRepository.cs b/Domain/Repository/IUserRepository.cs
new file mode 100644
index 0000000..124029b
--- /dev/null
+++ b/Domain/Repository/IUserRepository.cs
@@ -0,0 +1,12 @@
+using Domain.Aggregates.User;
+
+namespace Domain.Repository;
+
+public interface IUserRepository
+{
+ Task AddUserAsync(User user);
+ Task GetUserByEmailAsync(string email);
+ Task IsUserExistsByIdAsync(Guid userId);
+ Task> GetAllUsersAsync();
+ Task GetUserByIdAsync(Guid userId);
+}
\ No newline at end of file
diff --git a/Application/Features/AppointmentServiceLink/Interfaces/IappointmenServiceLinkRepository.cs b/Domain/Repository/IappointmenServiceLinkRepository.cs
similarity index 81%
rename from Application/Features/AppointmentServiceLink/Interfaces/IappointmenServiceLinkRepository.cs
rename to Domain/Repository/IappointmenServiceLinkRepository.cs
index 103cf74..e27cdf3 100644
--- a/Application/Features/AppointmentServiceLink/Interfaces/IappointmenServiceLinkRepository.cs
+++ b/Domain/Repository/IappointmenServiceLinkRepository.cs
@@ -1,4 +1,4 @@
-namespace Application.Features.AppointmentServiceLink.Interfaces;
+namespace Domain.Repository;
public interface IAppointmenServiceLinkRepository
{
diff --git a/Domain/ValueObject/AppointmentTimeRange.cs b/Domain/ValueObject/AppointmentTimeRange.cs
new file mode 100644
index 0000000..15af97b
--- /dev/null
+++ b/Domain/ValueObject/AppointmentTimeRange.cs
@@ -0,0 +1,17 @@
+namespace Domain.ValueObject;
+
+public class AppointmentTimeRange
+{
+ public DateTime StartTime { get; private set; }
+ public DateTime EndTime { get; private set; }
+
+ public AppointmentTimeRange(DateTime startTime, DateTime endTime)
+ {
+ if (startTime == default || endTime == default)
+ throw new ArgumentException("Start and End are required");
+ if(startTime >= endTime)
+ throw new ArgumentException("Start time must be before end time");
+ StartTime = startTime;
+ EndTime = endTime;
+ }
+}
\ No newline at end of file
diff --git a/Domain/ValueObject/Email.cs b/Domain/ValueObject/Email.cs
new file mode 100644
index 0000000..f10888d
--- /dev/null
+++ b/Domain/ValueObject/Email.cs
@@ -0,0 +1,36 @@
+using System.Text.RegularExpressions;
+
+namespace Domain.ValueObject;
+
+public class Email
+{
+ public string Value { get; private set; }
+
+ private Email()
+ {
+ }
+
+ public Email(string value)
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ throw new ArgumentException("Value cannot be null or whitespace.");
+
+ if (!IsValidEmail(value))
+ throw new ArgumentException("Value must be a valid email address.");
+
+ Value = value.ToLowerInvariant();
+ }
+
+ private static bool IsValidEmail(string email)
+ {
+ var pattern = @"^[^@\s]+@[^@\s]+\.[^@\s]+$";
+ return Regex.IsMatch(email, pattern, RegexOptions.IgnoreCase);
+ }
+
+ public override bool Equals(object? obj)
+ {
+ if (obj is not Email other) return false;
+ return Value == other.Value;
+ }
+ public override string ToString() => Value;
+}
\ No newline at end of file
diff --git a/Domain/ValueObject/Password.cs b/Domain/ValueObject/Password.cs
new file mode 100644
index 0000000..aa40f3e
--- /dev/null
+++ b/Domain/ValueObject/Password.cs
@@ -0,0 +1,37 @@
+using System.Text.RegularExpressions;
+using Domain.Entities;
+
+namespace Domain.ValueObject;
+
+public class Password
+{
+ public string Hash { get; private set; }
+
+ private Password()
+ {
+
+ }
+ public Password(string hash)
+ {
+ if(hash == null)
+ throw new ArgumentNullException(nameof(hash));
+
+ Hash = hash;
+ }
+ public static Password FromPlainText(string plainText, IHasher hasher)
+ {
+ if (!IsValidPlainText(plainText))
+ throw new ArgumentException("Invalid password format");
+ return new Password(hasher.Hash(plainText));
+ }
+ public static bool IsValidPlainText(string plainText)
+ {
+ if (string.IsNullOrWhiteSpace(plainText))
+ return false;
+
+ var pattern = @"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$";
+ return Regex.IsMatch(plainText, pattern);
+ }
+ public bool Verify(string plainText, IHasher hasher)
+ => hasher.Verify(Hash, plainText);
+}
\ No newline at end of file
diff --git a/Domain/ValueObject/PhoneNumber.cs b/Domain/ValueObject/PhoneNumber.cs
new file mode 100644
index 0000000..bdf54e9
--- /dev/null
+++ b/Domain/ValueObject/PhoneNumber.cs
@@ -0,0 +1,26 @@
+using System.Text.RegularExpressions;
+
+namespace Domain.ValueObject;
+
+public class PhoneNumber
+{
+ public string Value { get; private set; }
+ private PhoneNumber()
+ {
+ }
+
+ public PhoneNumber(string value)
+ {
+ if (value == null)
+ throw new ArgumentNullException(nameof(value));
+
+ if(string.IsNullOrWhiteSpace(value))
+ throw new ArgumentException("Phone number cannot be empty or whitespace only string.", nameof(value));
+ Value = value;
+ }
+ private static bool IsValidPhoneNumber(string number)
+ {
+ var pattern = @"^09[0-9]{9}$";
+ return Regex.IsMatch(number, pattern);
+ }
+}
\ No newline at end of file
diff --git a/Infrastructure/DependencyInjection.cs b/Infrastructure/DependencyInjection.cs
index bc3d3f9..68a1cc9 100644
--- a/Infrastructure/DependencyInjection.cs
+++ b/Infrastructure/DependencyInjection.cs
@@ -2,9 +2,8 @@
using Application.Common.Interfaces;
using Application.Common.Interfaces.Repositories;
using Application.Features.Appointments.Interfaces;
-using Application.Features.AppointmentServiceLink.Interfaces;
using Application.Features.Auth.Interfaces;
-using Application.Features.Service.Interfaces;
+using Domain.Repository;
using Infrastructure.Persistance;
using Infrastructure.Persistance.Repositories;
using Infrastructure.Security;
@@ -32,7 +31,7 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi
services.AddScoped();
services.AddScoped();
services.AddScoped();
- services.AddScoped();
+ services.AddScoped();
services.AddScoped();
return services;
diff --git a/Infrastructure/Persistance/AppDbContext.cs b/Infrastructure/Persistance/AppDbContext.cs
index b35ff05..d4f4aff 100644
--- a/Infrastructure/Persistance/AppDbContext.cs
+++ b/Infrastructure/Persistance/AppDbContext.cs
@@ -1,3 +1,6 @@
+using Domain.Aggregates.Appointment;
+using Domain.Aggregates.Service;
+using Domain.Aggregates.User;
using Domain.Entities;
using Infrastructure.Persistance.Configurations;
using Microsoft.EntityFrameworkCore;
diff --git a/Infrastructure/Persistance/Configurations/AppointmentConfiguration.cs b/Infrastructure/Persistance/Configurations/AppointmentConfiguration.cs
index 432b077..c4784bf 100644
--- a/Infrastructure/Persistance/Configurations/AppointmentConfiguration.cs
+++ b/Infrastructure/Persistance/Configurations/AppointmentConfiguration.cs
@@ -1,3 +1,4 @@
+using Domain.Aggregates.Appointment;
using Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
diff --git a/Infrastructure/Persistance/Configurations/ServiceConfiguration.cs b/Infrastructure/Persistance/Configurations/ServiceConfiguration.cs
index 631ab74..3ad9542 100644
--- a/Infrastructure/Persistance/Configurations/ServiceConfiguration.cs
+++ b/Infrastructure/Persistance/Configurations/ServiceConfiguration.cs
@@ -1,3 +1,4 @@
+using Domain.Aggregates.Service;
using Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
diff --git a/Infrastructure/Persistance/Configurations/UserConfiguration.cs b/Infrastructure/Persistance/Configurations/UserConfiguration.cs
index cda361c..70adbbb 100644
--- a/Infrastructure/Persistance/Configurations/UserConfiguration.cs
+++ b/Infrastructure/Persistance/Configurations/UserConfiguration.cs
@@ -1,3 +1,4 @@
+using Domain.Aggregates.User;
using Domain.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
diff --git a/Infrastructure/Persistance/DbInitializer.cs b/Infrastructure/Persistance/DbInitializer.cs
index eb63cd1..0cf98c2 100644
--- a/Infrastructure/Persistance/DbInitializer.cs
+++ b/Infrastructure/Persistance/DbInitializer.cs
@@ -1,4 +1,5 @@
using Application.Common;
+using Domain.Aggregates.User;
using Domain.Entities;
using Domain.Enums;
using Microsoft.Extensions.Configuration;
diff --git a/Infrastructure/Persistance/Repositories/AppointmentRepository.cs b/Infrastructure/Persistance/Repositories/AppointmentRepository.cs
index dd771f4..cff6b82 100644
--- a/Infrastructure/Persistance/Repositories/AppointmentRepository.cs
+++ b/Infrastructure/Persistance/Repositories/AppointmentRepository.cs
@@ -1,6 +1,8 @@
using Application.Features.Appointments.DTOs;
using Application.Features.Appointments.Interfaces;
+using Domain.Aggregates.Appointment;
using Domain.Entities;
+using Domain.Repository;
using Microsoft.EntityFrameworkCore;
namespace Infrastructure.Persistance.Repositories;
diff --git a/Infrastructure/Persistance/Repositories/AppointmentServiceLinkRepository.cs b/Infrastructure/Persistance/Repositories/AppointmentServiceLinkRepository.cs
index 517d4ed..4043c1a 100644
--- a/Infrastructure/Persistance/Repositories/AppointmentServiceLinkRepository.cs
+++ b/Infrastructure/Persistance/Repositories/AppointmentServiceLinkRepository.cs
@@ -1,5 +1,5 @@
-using Application.Features.AppointmentServiceLink.Interfaces;
using Domain.Entities;
+using Domain.Repository;
using Microsoft.EntityFrameworkCore;
namespace Infrastructure.Persistance.Repositories;
diff --git a/Infrastructure/Persistance/Repositories/ServiceRepository.cs b/Infrastructure/Persistance/Repositories/ServiceRepository.cs
index 36191b9..0a38b59 100644
--- a/Infrastructure/Persistance/Repositories/ServiceRepository.cs
+++ b/Infrastructure/Persistance/Repositories/ServiceRepository.cs
@@ -1,6 +1,7 @@
using Application.Features.Service.DTOs;
-using Application.Features.Service.Interfaces;
+using Domain.Aggregates.Service;
using Domain.Entities;
+using Domain.Repository;
using Microsoft.EntityFrameworkCore;
namespace Infrastructure.Persistance.Repositories;
diff --git a/Infrastructure/Persistance/Repositories/UserRepository.cs b/Infrastructure/Persistance/Repositories/UserRepository.cs
index faa8485..cd63a1e 100644
--- a/Infrastructure/Persistance/Repositories/UserRepository.cs
+++ b/Infrastructure/Persistance/Repositories/UserRepository.cs
@@ -1,6 +1,8 @@
using Application.Features.Auth.DTOs;
using Application.Features.Auth.Interfaces;
+using Domain.Aggregates.User;
using Domain.Entities;
+using Domain.Repository;
using Microsoft.EntityFrameworkCore;
namespace Infrastructure.Persistance.Repositories;
diff --git a/Infrastructure/Security/Context/UserContext.cs b/Infrastructure/Security/Context/UserContext.cs
index 3255df5..e05ea79 100644
--- a/Infrastructure/Security/Context/UserContext.cs
+++ b/Infrastructure/Security/Context/UserContext.cs
@@ -5,7 +5,7 @@
namespace Infrastructure.Security.Context;
-public class UserContext : IUSerContext
+public class UserContext : IUserContext
{
private readonly IHttpContextAccessor _httpContextAccessor;
diff --git a/Infrastructure/Security/Jwt/JwtTokenService.cs b/Infrastructure/Security/Jwt/JwtTokenService.cs
index 5aca60d..6368aaf 100644
--- a/Infrastructure/Security/Jwt/JwtTokenService.cs
+++ b/Infrastructure/Security/Jwt/JwtTokenService.cs
@@ -3,6 +3,7 @@
using System.Security.Cryptography;
using System.Text;
using Application.Common.Interfaces;
+using Domain.Aggregates.User;
using Domain.Entities;
using Microsoft.Extensions.Configuration;
using Microsoft.IdentityModel.Tokens;
@@ -32,7 +33,7 @@ public string GenerateJwtToken(User user)
Encoding.UTF8.GetBytes(_configuration["JwtSettings:SecretKey"])
);
- var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
+ var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var expiresMinutes = int.Parse(_configuration["JwtSettings:AccessTokenMinutes"]);
var token = new JwtSecurityToken(
diff --git a/api/Controllers/UserController.cs b/api/Controllers/UserController.cs
index 02711cb..eeab311 100644
--- a/api/Controllers/UserController.cs
+++ b/api/Controllers/UserController.cs
@@ -60,4 +60,5 @@ public async Task UpdateProfile([FromBody] UpdateProfileRequ
{
return await _service.UpdateProfileAsync(updateProfileRequestDto);
}
+
}
\ No newline at end of file
diff --git a/api/Program.cs b/api/Program.cs
index a9c0357..5ec38c8 100644
--- a/api/Program.cs
+++ b/api/Program.cs
@@ -53,14 +53,13 @@
{
options.RequireHttpsMetadata = false;
options.SaveToken = true;
-
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
-
+
ValidIssuer = builder.Configuration["JwtSettings:Issuer"],
ValidAudience = builder.Configuration["JwtSettings:Audience"],