diff --git a/Trimly.Core.Application/AddApplication.cs b/Trimly.Core.Application/AddApplication.cs index 3e4a047..fe42430 100644 --- a/Trimly.Core.Application/AddApplication.cs +++ b/Trimly.Core.Application/AddApplication.cs @@ -1,4 +1,6 @@ using Microsoft.Extensions.DependencyInjection; +using Trimly.Core.Application.Interfaces.Service; +using Trimly.Core.Application.Services; namespace Trimly.Core.Application { @@ -6,6 +8,11 @@ public static class AddApplication { public static IServiceCollection AddApplicationLayer(this IServiceCollection services) { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); return services; } } diff --git a/Trimly.Core.Application/DTOs/Appointment/AppointmentDTos.cs b/Trimly.Core.Application/DTOs/Appointment/AppointmentDTos.cs index 8cc5831..f0aff24 100644 --- a/Trimly.Core.Application/DTOs/Appointment/AppointmentDTos.cs +++ b/Trimly.Core.Application/DTOs/Appointment/AppointmentDTos.cs @@ -7,8 +7,6 @@ public sealed record AppointmentDTos DateTime? StartDateTime, DateTime? EndDateTime, AppointmentStatus AppointmentStatus, - string? ConfirmationCode, - string? CancellationReason, Guid? ServiceId, DateTime? CreatedAt, DateTime? UpdateAt diff --git a/Trimly.Core.Application/DTOs/Appointment/ConfirmAppointmentDTos.cs b/Trimly.Core.Application/DTOs/Appointment/ConfirmAppointmentDTos.cs new file mode 100644 index 0000000..30cfcd5 --- /dev/null +++ b/Trimly.Core.Application/DTOs/Appointment/ConfirmAppointmentDTos.cs @@ -0,0 +1,10 @@ +using Trimly.Core.Domain.Enum; + +namespace Trimly.Core.Application.DTOs.Appointment; + +public sealed record ConfirmAppointmentDTos +( + AppointmentStatus AppointmentStatus, + DateTime? StartDateTime, + DateTime? EndDateTime +); \ No newline at end of file diff --git a/Trimly.Core.Application/DTOs/Appointment/CreateAppointmentDTos.cs b/Trimly.Core.Application/DTOs/Appointment/CreateAppointmentDTos.cs index c87fb5e..07a070d 100644 --- a/Trimly.Core.Application/DTOs/Appointment/CreateAppointmentDTos.cs +++ b/Trimly.Core.Application/DTOs/Appointment/CreateAppointmentDTos.cs @@ -5,10 +5,8 @@ namespace Trimly.Core.Application.DTOs.Appointment { public sealed record CreateAppointmentDTos ( - DateTime? StarDateTime, - DateTime? EndDateTime, - AppointmentStatus AppointmentStatus, - string? CancellationReason, + DateTime StarDateTime, + DateTime EndDateTime, Guid? ServiceId ); } diff --git a/Trimly.Core.Application/DTOs/Appointment/UpdateAppoinmentDTos.cs b/Trimly.Core.Application/DTOs/Appointment/UpdateAppoinmentDTos.cs index 620afa9..f6437e5 100644 --- a/Trimly.Core.Application/DTOs/Appointment/UpdateAppoinmentDTos.cs +++ b/Trimly.Core.Application/DTOs/Appointment/UpdateAppoinmentDTos.cs @@ -5,7 +5,6 @@ public sealed record UpdateAppoinmentDTos ( DateTime? StarDateTime, DateTime? EndDateTime, - string? CancellationReason, Guid? ServiceId ); } diff --git a/Trimly.Core.Application/DTOs/RegisteredCompanies/CreateRegisteredCompaniesDTos.cs b/Trimly.Core.Application/DTOs/RegisteredCompanies/CreateRegisteredCompaniesDTos.cs index 4150155..572d2ad 100644 --- a/Trimly.Core.Application/DTOs/RegisteredCompanies/CreateRegisteredCompaniesDTos.cs +++ b/Trimly.Core.Application/DTOs/RegisteredCompanies/CreateRegisteredCompaniesDTos.cs @@ -1,4 +1,5 @@ - +using Microsoft.AspNetCore.Http; + namespace Trimly.Core.Application.DTOs.RegisteredCompanies { public sealed record CreateRegisteredCompaniesDTos @@ -9,7 +10,6 @@ public sealed record CreateRegisteredCompaniesDTos string? AddresCompanies, string? Email, string? DescriptionCompanies, - string? LogoUrl, - Domain.Enum.Status? Status + IFormFile? ImageFile ); } diff --git a/Trimly.Core.Application/DTOs/Review/CreateReviewsDTos.cs b/Trimly.Core.Application/DTOs/Review/CreateReviewsDTos.cs index 7b23da3..9f719ea 100644 --- a/Trimly.Core.Application/DTOs/Review/CreateReviewsDTos.cs +++ b/Trimly.Core.Application/DTOs/Review/CreateReviewsDTos.cs @@ -5,6 +5,7 @@ public sealed record CreateReviewsDTos ( string? Title, string? Comment, - int Rating + int Rating, + Guid? RegisteredCompanyId ); } diff --git a/Trimly.Core.Application/DTOs/Schedules/CreateSchedulesDTos.cs b/Trimly.Core.Application/DTOs/Schedules/CreateSchedulesDTos.cs index cba1785..10d3f09 100644 --- a/Trimly.Core.Application/DTOs/Schedules/CreateSchedulesDTos.cs +++ b/Trimly.Core.Application/DTOs/Schedules/CreateSchedulesDTos.cs @@ -3,11 +3,11 @@ namespace Trimly.Core.Application.DTOs.Schedules { public sealed record CreateSchedulesDTos ( - Weekday? Days, + Weekday Days, TimeOnly OpeningTime, TimeOnly ClosingTime, string? Notes, - Status? IsHolady, + Status? IsHoliday, Guid? RegisteredCompanyId ); } diff --git a/Trimly.Core.Application/DTOs/Schedules/SchedulesDTos.cs b/Trimly.Core.Application/DTOs/Schedules/SchedulesDTos.cs index 626d521..bfc01ec 100644 --- a/Trimly.Core.Application/DTOs/Schedules/SchedulesDTos.cs +++ b/Trimly.Core.Application/DTOs/Schedules/SchedulesDTos.cs @@ -4,12 +4,12 @@ namespace Trimly.Core.Application.DTOs.Schedules { public sealed record SchedulesDTos ( - Guid? ShedulesId, + Guid? SchedulesId, Weekday? Days, TimeOnly OpeningTime, TimeOnly ClosingTime, string? Notes, - Status? IsHolady, + Status? IsHoliday, Guid? RegisteredCompanyId, DateTime? CreatedAt, DateTime? UpdateAt diff --git a/Trimly.Core.Application/DTOs/Schedules/UpdateSchedulesDTos.cs b/Trimly.Core.Application/DTOs/Schedules/UpdateSchedulesDTos.cs index 1954373..c227277 100644 --- a/Trimly.Core.Application/DTOs/Schedules/UpdateSchedulesDTos.cs +++ b/Trimly.Core.Application/DTOs/Schedules/UpdateSchedulesDTos.cs @@ -3,10 +3,10 @@ namespace Trimly.Core.Application.DTOs.Schedules { public sealed record UpdateSchedulesDTos ( - Weekday? Days, + Weekday Days, TimeOnly OpeningTime, TimeOnly ClosingTime, string? Notes, - Status? IsHolady + Status? IsHoliday ); } diff --git a/Trimly.Core.Application/DTOs/Service/CreateServiceDTos.cs b/Trimly.Core.Application/DTOs/Service/CreateServiceDTos.cs index 2ae9b73..439aa9d 100644 --- a/Trimly.Core.Application/DTOs/Service/CreateServiceDTos.cs +++ b/Trimly.Core.Application/DTOs/Service/CreateServiceDTos.cs @@ -1,4 +1,6 @@  +using Microsoft.AspNetCore.Http; + namespace Trimly.Core.Application.DTOs.Service { public sealed record CreateServiceDTos @@ -7,8 +9,7 @@ public sealed record CreateServiceDTos decimal Price, string? Description, int DurationInMinutes, - string? ImageUrl, - Guid? RegisteredCompanyId, - decimal PenaltyAmount + IFormFile? ImageFile, + Guid? RegisteredCompanyId ); } diff --git a/Trimly.Core.Application/DTOs/Service/ServiceFilterDTos.cs b/Trimly.Core.Application/DTOs/Service/ServiceFilterDTos.cs index 4c38cb9..efc0193 100644 --- a/Trimly.Core.Application/DTOs/Service/ServiceFilterDTos.cs +++ b/Trimly.Core.Application/DTOs/Service/ServiceFilterDTos.cs @@ -7,9 +7,6 @@ public sealed record ServiceFilterDTos decimal Price, string? Description, int DurationInMinutes, - string? ImageUrl, - Guid? RegisteredCompanyId, - DateTime? CreatedAt, - DateTime? UpdateAt + string? ImageUrl ); } diff --git a/Trimly.Core.Application/DTOs/Service/ServicesDTos.cs b/Trimly.Core.Application/DTOs/Service/ServicesDTos.cs index b396718..1bd5ae1 100644 --- a/Trimly.Core.Application/DTOs/Service/ServicesDTos.cs +++ b/Trimly.Core.Application/DTOs/Service/ServicesDTos.cs @@ -12,7 +12,6 @@ public sealed record ServicesDTos Domain.Enum.Status? Status, Guid? RegisteredCompanyId, DateTime? CreatedAt, - DateTime? UpdateAt, - decimal PenaltyAmount + DateTime? UpdateAt ); } diff --git a/Trimly.Core.Application/DTOs/Service/UpdateServiceDTos.cs b/Trimly.Core.Application/DTOs/Service/UpdateServiceDTos.cs index dea9525..70f87bb 100644 --- a/Trimly.Core.Application/DTOs/Service/UpdateServiceDTos.cs +++ b/Trimly.Core.Application/DTOs/Service/UpdateServiceDTos.cs @@ -6,7 +6,6 @@ public sealed record UpdateServiceDTos string? Name, decimal Price, string? Description, - int DurationInMinutes, - decimal PenaltyAmount + int DurationInMinutes ); } diff --git a/Trimly.Core.Application/Interfaces/Repository/IAppointmentRepository.cs b/Trimly.Core.Application/Interfaces/Repository/IAppointmentRepository.cs index e5cf14d..8b2efc2 100644 --- a/Trimly.Core.Application/Interfaces/Repository/IAppointmentRepository.cs +++ b/Trimly.Core.Application/Interfaces/Repository/IAppointmentRepository.cs @@ -14,7 +14,9 @@ public interface IAppointmentRepository : IGenericRepository Task ConfirmAppointmentAutomaticallyAsync(Appointments appointments,CancellationToken cancellationToken); Task GetTotalAppointmentCountAsync(Guid serviceId, CancellationToken cancellationToken); - + Task> FilterByStatusAsync(AppointmentStatus appointmentStatus, CancellationToken cancellationToken); + + Task ValidateAppointmentAsync(DateTime startDate, DateTime endDate, CancellationToken cancellationToken); } } diff --git a/Trimly.Core.Application/Interfaces/Repository/IGenericRepository.cs b/Trimly.Core.Application/Interfaces/Repository/IGenericRepository.cs index 5f991b2..925a793 100644 --- a/Trimly.Core.Application/Interfaces/Repository/IGenericRepository.cs +++ b/Trimly.Core.Application/Interfaces/Repository/IGenericRepository.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System.Linq.Expressions; +using System.Threading; using Trimly.Core.Application.Pagination; namespace Trimly.Core.Application.Interfaces.Repository @@ -17,5 +18,7 @@ public interface IGenericRepository Task> GetPagedResultAsync(int pageNumber,int pageSize, CancellationToken cancellationToken); Task SaveChangesAsync(CancellationToken cancellationToken); + + Task ValidateAsync(Expression> predicate); } } diff --git a/Trimly.Core.Application/Interfaces/Repository/ISchedulesRepository.cs b/Trimly.Core.Application/Interfaces/Repository/ISchedulesRepository.cs index 630789e..56f9aea 100644 --- a/Trimly.Core.Application/Interfaces/Repository/ISchedulesRepository.cs +++ b/Trimly.Core.Application/Interfaces/Repository/ISchedulesRepository.cs @@ -12,5 +12,7 @@ public interface ISchedulesRepository : IGenericRepository Task> FilterByWeekDayAsync(Weekday weekday, CancellationToken cancellationToken); Task> GetSchedulesByCompanyId(Guid registeredCompanyId, CancellationToken cancellationToken); + + Task GetScheduleByCompanyIdAsync(Guid companyId, CancellationToken cancellationToken); } } diff --git a/Trimly.Core.Application/Interfaces/Repository/IServiceRepository.cs b/Trimly.Core.Application/Interfaces/Repository/IServiceRepository.cs index d4670d1..3415ce3 100644 --- a/Trimly.Core.Application/Interfaces/Repository/IServiceRepository.cs +++ b/Trimly.Core.Application/Interfaces/Repository/IServiceRepository.cs @@ -2,17 +2,19 @@ namespace Trimly.Core.Application.Interfaces.Repository { - public interface IServiceRepository : IGenericRepository + public interface IServiceRepository : IGenericRepository { - Task> GetServicesByNameAsync(Guid registeredCompaniesId, string name,CancellationToken cancellationToken); + Task> GetServicesByNameAsync(Guid registeredCompaniesId, string name,CancellationToken cancellationToken); - Task> GetServicesByPriceAsync(Guid registeredCompaniesId, decimal price, CancellationToken cancellationToken); + Task> GetServicesByPriceAsync(Guid registeredCompaniesId, decimal price, CancellationToken cancellationToken); - Task> GetServicesByDurationInMinutesAsync(Guid registeredCompaniesId, int durationInMinutes, CancellationToken cancellationToken); + Task> GetServicesByDurationInMinutesAsync(Guid registeredCompaniesId, int durationInMinutes, CancellationToken cancellationToken); - Task ApplyDiscountCodeAsync(Services services, Guid registeredCompaniesId, string discountCode ,CancellationToken cancellationToken); + Task ApplyDiscountCodeAsync(Domain.Models.Services services, Guid registeredCompaniesId, string discountCode ,CancellationToken cancellationToken); - Task> GetServicesWithDurationLessThan30MinutesAsync(Guid registeredCompaniesId, CancellationToken cancellationToken); + Task> GetServicesWithDurationLessThan30MinutesAsync(Guid registeredCompaniesId, CancellationToken cancellationToken); + + Task> GetServicesByCompanyIdAsync(Guid companyId, CancellationToken cancellationToken); } } diff --git a/Trimly.Core.Application/Interfaces/Service/IAppointmentService.cs b/Trimly.Core.Application/Interfaces/Service/IAppointmentService.cs index e630942..ed8c0f6 100644 --- a/Trimly.Core.Application/Interfaces/Service/IAppointmentService.cs +++ b/Trimly.Core.Application/Interfaces/Service/IAppointmentService.cs @@ -16,14 +16,12 @@ public interface IAppointmentService : IGenericService< Task> RescheduleAppointmentAsync(Guid appointmentId, RescheduleAppointmentDTos rescheduleAppointment, CancellationToken cancellationToken); - Task> ConfirmAppointmentAutomaticallyAsync(AppointmentStatus appointmentStatus, CancellationToken cancellationToken); + Task> ConfirmAppointmentAutomaticallyAsync(Guid appointmentId, CancellationToken cancellationToken); Task> CancelAppointmentWithoutPenaltyAsync(Guid appointmentId, string cancellationReason, CancellationToken cancellationToken); - - Task>> GetAvailableAppointmentsAsync(AppointmentDateFilterType filterType, CancellationToken cancellationToken); - + Task> GetTotalAppointmentsCountAsync(Guid serviceId, CancellationToken cancellationToken); - - Task> ConfirmAppointment(Guid appointmentId, string confirmationCode, CancellationToken cancellationToken); + Task> CancelAppointmentWithPenaltyAsync(Guid appointmentId, double penalizationPorcentage ,CancellationToken cancellationToken); + Task> ConfirmAppointment(Guid appointmentId, string confirmationCode, CancellationToken cancellationToken); } } diff --git a/Trimly.Core.Application/Interfaces/Service/ICodeConfirmationService.cs b/Trimly.Core.Application/Interfaces/Service/ICodeConfirmationService.cs new file mode 100644 index 0000000..e82d1a5 --- /dev/null +++ b/Trimly.Core.Application/Interfaces/Service/ICodeConfirmationService.cs @@ -0,0 +1,6 @@ +namespace Trimly.Core.Application.Interfaces.Service; + +public interface ICodeConfirmationService +{ + Task GenerateCodeConfirmation(string emailAddress); +} \ No newline at end of file diff --git a/Trimly.Core.Application/Interfaces/Service/IGenericService.cs b/Trimly.Core.Application/Interfaces/Service/IGenericService.cs index 25f4ddb..08b87da 100644 --- a/Trimly.Core.Application/Interfaces/Service/IGenericService.cs +++ b/Trimly.Core.Application/Interfaces/Service/IGenericService.cs @@ -15,7 +15,7 @@ public interface IGenericService Task> GetByIdAsync(Guid id, CancellationToken cancellationToken); - Task> CreateAsync(TCreateDTo entity, CancellationToken cancellationToken); + Task> CreateAsync(TCreateDTo entityCreateDTo, CancellationToken cancellationToken); Task> UpdateAsync(Guid id, TUpdateDTo entity,CancellationToken cancellation); diff --git a/Trimly.Core.Application/Interfaces/Service/ISchedulesService.cs b/Trimly.Core.Application/Interfaces/Service/ISchedulesService.cs index b8fbe4b..d7f9a98 100644 --- a/Trimly.Core.Application/Interfaces/Service/ISchedulesService.cs +++ b/Trimly.Core.Application/Interfaces/Service/ISchedulesService.cs @@ -9,11 +9,11 @@ public interface ISchedulesService : IGenericService< UpdateSchedulesDTos, SchedulesDTos> { - Task> ActivatedIsHolidayAsync(CancellationToken cancellationToken); + Task> ActivatedIsHolidayAsync(Guid registeredCompany,CancellationToken cancellationToken); Task>> FilterByOpeningTimeAsync(Guid registeredCompany, TimeOnly openingTime, CancellationToken cancellationToken); - Task>> FilterByIsHolidayAsync(Guid registeredCompany, CancellationToken cancellationToken); + Task>> FilterByIsHolidayAsync(Guid registeredCompany, Status isHoliday ,CancellationToken cancellationToken); Task>> FilterByWeekDayAsync(Guid registeredCompany, Weekday weekday, CancellationToken cancellationToken); diff --git a/Trimly.Core.Application/Interfaces/Service/IServicesService.cs b/Trimly.Core.Application/Interfaces/Service/IServicesService.cs index 785fc4c..0194b52 100644 --- a/Trimly.Core.Application/Interfaces/Service/IServicesService.cs +++ b/Trimly.Core.Application/Interfaces/Service/IServicesService.cs @@ -8,7 +8,7 @@ public interface IServicesService : IGenericService< UpdateServiceDTos, ServicesDTos> { - Task> ApplyDiscountCodeAsync(Guid serviceId,Guid registeredCompanyId, double discount, string discountCode, CancellationToken cancellationToken); + Task> ApplyDiscountCodeAsync(Guid serviceId,Guid registeredCompanyId, string discountCode, CancellationToken cancellationToken); Task>> GetServicesWithDurationLessThan30MinutesAsync(Guid registeredCompany, CancellationToken cancellationToken); @@ -16,6 +16,6 @@ public interface IServicesService : IGenericService< Task>> GetServicesByNameAsync(string name, Guid registeredCompanyId, CancellationToken cancellationToken); - Task>> GetServicesByPriceAsync(double price, Guid registeredCompanyId, CancellationToken cancellationToken); + Task>> GetServicesByPriceAsync(decimal price, Guid registeredCompanyId, CancellationToken cancellationToken); } } diff --git a/Trimly.Core.Application/Services/AppointmentService.cs b/Trimly.Core.Application/Services/AppointmentService.cs new file mode 100644 index 0000000..717473b --- /dev/null +++ b/Trimly.Core.Application/Services/AppointmentService.cs @@ -0,0 +1,370 @@ +using System.Collections; +using Microsoft.Extensions.Logging; +using Trimly.Core.Application.DTOs.Appointment; +using Trimly.Core.Application.Interfaces.Repository; +using Trimly.Core.Application.Interfaces.Service; +using Trimly.Core.Application.Pagination; +using Trimly.Core.Domain.Enum; +using Trimly.Core.Domain.Utils; + +namespace Trimly.Core.Application.Services; +public class AppointmentService : IAppointmentService +{ + private readonly IAppointmentRepository _repository; + private readonly ILogger _logger; + public AppointmentService(IAppointmentRepository repository, ILogger logger) + { + _repository = repository; + _logger = logger; + } + + public async Task>> GetPagedResult(int pageNumber, int pageSize, CancellationToken cancellationToken) + { + if (pageNumber <= 0 && pageSize <= 0) + { + _logger.LogError("Invalid pagination parameters: pageNumber and pageSize must be greater than 1."); + + return ResultT>.Failure(Error.Failure("400", "Invalid pagination parameters")); + } + + var appointmentPaged = await _repository.GetPagedResultAsync(pageNumber, pageSize, cancellationToken); + var dtoItems = appointmentPaged.Items.Select(x => new AppointmentDTos + ( + AppointmentId: x.AppointmentId, + StartDateTime: x.StartDateTime, + EndDateTime: x.EndDateTime, + AppointmentStatus: x.AppointmentStatus, + ServiceId: x.ServiceId, + CreatedAt: x.CreatedAt, + UpdateAt: x.UpdateAt + )).ToList(); + + if (!dtoItems.Any()) + { + _logger.LogWarning("No appointments found for the specified parameters. Page: {PageNumber}, Page Size: {PageSize}", pageNumber, pageSize); + + return ResultT>.Failure(Error.Failure("400", "No available appointments found for the specified criteria.")); + } + + PagedResult pagedResult = new() + { + TotalItems = appointmentPaged.TotalItems, + CurrentPage = appointmentPaged.CurrentPage, + TotalPages = appointmentPaged.TotalPages, + Items = dtoItems, + }; + + _logger.LogInformation("Returning paged result with {TotalItems} appointments. Page {CurrentPage} of {TotalPages}.", + appointmentPaged.TotalItems, appointmentPaged.CurrentPage, appointmentPaged.TotalPages); + + return ResultT>.Success(pagedResult); + } + + public async Task> GetByIdAsync(Guid id, CancellationToken cancellationToken) + { + var appointment = await _repository.GetByIdAsync(id, cancellationToken); + if (appointment == null) + { + _logger.LogWarning("No appointment found for the specified {id}.",id); + + return ResultT.Failure(Error.NotFound("404", "No appointment found for the specified.")); + } + + AppointmentDTos appointmentDTos = new AppointmentDTos + ( + AppointmentId: appointment.AppointmentId, + StartDateTime: appointment.StartDateTime, + EndDateTime: appointment.EndDateTime, + AppointmentStatus: appointment.AppointmentStatus, + ServiceId: appointment.ServiceId, + CreatedAt: appointment.CreatedAt, + UpdateAt: appointment.UpdateAt + ); + + _logger.LogInformation("Returning appointment details. AppointmentId: {AppointmentId}, StartDateTime: {StartDateTime}, EndDateTime: {EndDateTime}, Status: {AppointmentStatus}", + appointment.AppointmentId, appointment.StartDateTime, appointment.EndDateTime, appointment.AppointmentStatus); + + return ResultT.Success(appointmentDTos); + } + + public async Task> CreateAsync(CreateAppointmentDTos entityCreateDTo, CancellationToken cancellationToken) + { + if (entityCreateDTo == null) + { + _logger.LogWarning("No appointment found matching the specified criteria. The request may contain invalid or non-existent appointment details."); + + return ResultT.Failure(Error.Failure("400", "No appointment matches the specified criteria. Please verify your request parameters.")); + } + + var exist = await _repository.ValidateAppointmentAsync(entityCreateDTo.StarDateTime, entityCreateDTo.EndDateTime,cancellationToken); + if (exist) + { + _logger.LogError("Failed to create appointment: The selected time slot ({0} - {1}) is already booked.", entityCreateDTo.StarDateTime, entityCreateDTo.EndDateTime); + + return ResultT.Failure(Error.Failure("400", + $"The selected time slot ({entityCreateDTo.StarDateTime} - {entityCreateDTo.EndDateTime}) is already booked. Please choose a different time.")); + } + + Domain.Models.Appointments appointments = new() + { + AppointmentId = Guid.NewGuid(), + StartDateTime = entityCreateDTo.StarDateTime, + EndDateTime = entityCreateDTo.EndDateTime, + ServiceId = entityCreateDTo.ServiceId + }; + + await _repository.AddAsync(appointments, cancellationToken); + + AppointmentDTos appointmentDTos = new + ( + AppointmentId: appointments.AppointmentId, + StartDateTime: appointments.StartDateTime, + EndDateTime: appointments.EndDateTime, + AppointmentStatus: AppointmentStatus.Pending, + ServiceId: appointments.ServiceId, + CreatedAt: appointments.CreatedAt, + UpdateAt : appointments.UpdateAt + ); + + _logger.LogInformation("Successfully retrieved appointment details. AppointmentId: {AppointmentId}, StartDateTime: {StartDateTime}, EndDateTime: {EndDateTime}, Status: {AppointmentStatus}", + appointmentDTos.AppointmentId, appointmentDTos.StartDateTime, appointmentDTos.EndDateTime, appointmentDTos.AppointmentStatus); + + return ResultT.Success(appointmentDTos); + } + + public async Task> UpdateAsync(Guid id, UpdateAppoinmentDTos entity, CancellationToken cancellation) + { + var appointment = await _repository.GetByIdAsync(id, cancellation); + if (appointment == null) + { + _logger.LogError("Appointment not found. AppointmentId: {AppointmentId}", appointment); + + return ResultT.Failure(Error.NotFound("404", "No appointment found for the specified ID.")); + } + + appointment.StartDateTime = entity.StarDateTime; + appointment.EndDateTime = entity.EndDateTime; + appointment.ServiceId = entity.ServiceId; + appointment.UpdateAt = DateTime.UtcNow; + + await _repository.UpdateAsync(appointment, cancellation); + + AppointmentDTos apointmentDTos = new + ( + AppointmentId: appointment.AppointmentId, + StartDateTime: appointment.StartDateTime, + EndDateTime: appointment.EndDateTime, + AppointmentStatus: appointment.AppointmentStatus, + ServiceId: appointment.ServiceId, + CreatedAt: appointment.CreatedAt, + UpdateAt : appointment.UpdateAt + ); + + _logger.LogInformation("Successfully retrieved appointment details for AppointmentId: {AppointmentId}. StartDateTime: {StartDateTime}, EndDateTime: {EndDateTime}, Status: {AppointmentStatus}.", + appointment.AppointmentId, appointment.StartDateTime, appointment.EndDateTime, appointment.AppointmentStatus); + + return ResultT.Success(apointmentDTos); + } + + public async Task> DeleteAsync(Guid id, CancellationToken cancellation) + { + var appointment = await _repository.GetByIdAsync(id, cancellation); + if (appointment == null) + { + _logger.LogError("No appointment found with AppointmentId: {AppointmentId}.", id); + + return ResultT.Failure(Error.NotFound("404", "Appointment not found.")); + } + + await _repository.DeleteAsync(appointment, cancellation); + + _logger.LogInformation("Successfully deleted appointment with AppointmentId: {AppointmentId}.", id); + + return ResultT.Success(appointment.AppointmentId ?? Guid.Empty); + } + + public async Task>> GetAppointmentsByStatusAsync(AppointmentStatus status, CancellationToken cancellationToken) + { + // Validate if the appointment status exists in the database + var exists = await _repository.ValidateAsync(x => x.AppointmentStatus == status); + if (!exists) + { + _logger.LogError("Appointment status '{Status}' is not valid or does not exist.", status); + + return ResultT>.Failure(Error.NotFound("404", $"Appointment status '{status}' is not valid or does not exist.")); + } + + var filterByStatus = await _repository.FilterByStatusAsync(status, cancellationToken); + if (filterByStatus == null || !filterByStatus.Any()) + { + _logger.LogError("No appointments found with status '{Status}'.", status); + + return ResultT>.Failure(Error.NotFound("404", $"No appointments found with status '{status}'.")); + } + + IEnumerable appointmentDTos = filterByStatus.Select(x => new AppointmentDTos + ( + AppointmentId: x.AppointmentId, + StartDateTime: x.StartDateTime, + EndDateTime: x.EndDateTime, + AppointmentStatus: x.AppointmentStatus, + ServiceId: x.ServiceId, + CreatedAt: x.CreatedAt, + UpdateAt: x.UpdateAt + )); + + _logger.LogInformation("{Count} appointments found with status '{Status}'.", appointmentDTos.Count(), status); + + return ResultT>.Success(appointmentDTos); + } + + public async Task> CancelAppointmentAsync(Guid appointmentId, CancellationToken cancellationToken) + { + var appointment = await _repository.GetByIdAsync(appointmentId, cancellationToken); + if (appointment == null) + { + _logger.LogError("No appointment found with AppointmentId: {AppointmentId}.", appointmentId); + + return ResultT.Failure(Error.NotFound("404", "Appointment not found.")); + } + + await _repository.CancelAppointmentAsync(appointment, cancellationToken); + + _logger.LogInformation("Successfully canceled appointment with AppointmentId: {AppointmentId}.", appointmentId); + + return ResultT.Success(appointment.AppointmentId ?? Guid.Empty); + } + + public async Task> RescheduleAppointmentAsync(Guid appointmentId, RescheduleAppointmentDTos rescheduleAppointment, + CancellationToken cancellationToken) + { + var appointment = await _repository.GetByIdAsync(appointmentId, cancellationToken); + if (appointment == null) + { + _logger.LogError("No appointment found with AppointmentId: {AppointmentId}.", appointmentId); + + return ResultT.Failure(Error.NotFound("404", "Appointment not found.")); + } + + appointment.UpdateAt = DateTime.UtcNow; + + await _repository.RescheduleAppointmentAsync(appointment,cancellationToken); + + _logger.LogInformation("Successfully rescheduled appointment with AppointmentId: {AppointmentId}.", appointmentId); + + return ResultT.Success(rescheduleAppointment); + } + + public async Task> ConfirmAppointmentAutomaticallyAsync(Guid appointmentId, CancellationToken cancellationToken) + { + var appointment = await _repository.GetByIdAsync(appointmentId, cancellationToken); + if (appointment == null) + { + _logger.LogError("No appointment found with AppointmentId: {AppointmentId}.", appointmentId); + + return ResultT.Failure(Error.NotFound("404", "Appointment not found.")); + } + + await _repository.ConfirmAppointmentAutomaticallyAsync(appointment, cancellationToken); + + _logger.LogInformation("Appointment with AppointmentId: {AppointmentId} has been successfully confirmed automatically.", appointmentId); + + return ResultT.Success(appointment.AppointmentId ?? Guid.Empty); + } + + public async Task> CancelAppointmentWithoutPenaltyAsync(Guid appointmentId, string cancellationReason, + CancellationToken cancellationToken) + { + var appointment = await _repository.GetByIdAsync(appointmentId, cancellationToken); + if (appointment == null) + { + _logger.LogError("Appointment not found for AppointmentId: {AppointmentId}.", appointmentId); + + return ResultT.Failure(Error.NotFound("404", $"Appointment with ID {appointmentId} not found.")); + } + + appointment.CancellationReason = cancellationReason; + appointment.UpdateAt = DateTime.UtcNow; + + await _repository.CancelAppointmentAsync(appointment, cancellationToken); + + _logger.LogInformation("Successfully canceled appointment with AppointmentId: {AppointmentId}.", appointmentId); + + return ResultT.Success(appointment.AppointmentId ?? Guid.Empty); + } + + public async Task> GetTotalAppointmentsCountAsync(Guid serviceId, CancellationToken cancellationToken) + { + var appointment = await _repository.GetByIdAsync(serviceId, cancellationToken); + if (appointment == null) + { + _logger.LogError("Service with ServiceId: {ServiceId} not found.", serviceId); + + return ResultT.Failure(Error.NotFound("404", "Service not found.")); + } + + var servicesCount = await _repository.GetTotalAppointmentCountAsync(serviceId, cancellationToken); + + _logger.LogInformation("Successfully retrieved total appointment count for ServiceId: {ServiceId}. Total appointments: {TotalAppointments}.", + serviceId, servicesCount); + + return ResultT.Success(servicesCount); + } + + public async Task> CancelAppointmentWithPenaltyAsync(Guid appointmentId, + double penalizationPorcentage, CancellationToken cancellationToken) + { + var appointment = await _repository.GetByIdAsync(appointmentId, cancellationToken); + if (appointment == null) + { + _logger.LogError("Appointment not found with AppointmentId: {AppointmentId}.", appointmentId); + + return ResultT.Failure(Error.NotFound("404", "Appointment not found.")); + } + if (penalizationPorcentage < 0) + { + _logger.LogError("Penalization porcentage cannot be less than zero."); + + return ResultT.Failure(Error.Failure("400", "Penalization percentage cannot be less than zero.")); + } + + await _repository.CancelAppointmentWithPenaltyAsync(appointment, penalizationPorcentage, cancellationToken); + + _logger.LogInformation("Appointment with AppointmentId: {AppointmentId} has been cancelled with a penalty of {PenalizationPercentage}%.", appointmentId, penalizationPorcentage); + + return ResultT.Success("Appointment successfully cancelled with penalty."); + } + + public async Task> ConfirmAppointment(Guid appointmentId, string confirmationCode, CancellationToken cancellationToken) + { + var appointment = await _repository.GetByIdAsync(appointmentId, cancellationToken); + if (appointment == null) + { + _logger.LogError("Failed to confirm appointment. No appointment found with ID: {AppointmentId}.", appointmentId); + + return ResultT.Failure(Error.NotFound("404", "The specified appointment does not exist.")); + } + + if (string.IsNullOrWhiteSpace(confirmationCode)) + { + _logger.LogError("Failed to confirm appointment. The confirmation code is missing or invalid."); + + return ResultT.Failure(Error.Failure("400", "A valid confirmation code is required.")); + } + + appointment.ConfirmationCode = confirmationCode; + appointment.AppointmentStatus = AppointmentStatus.Confirmed; + await _repository.UpdateAsync(appointment, cancellationToken); + + ConfirmAppointmentDTos confirmAppointmentDTos = new + ( + AppointmentStatus: appointment.AppointmentStatus, + StartDateTime: appointment.StartDateTime, + EndDateTime: appointment.EndDateTime + ); + + _logger.LogInformation("Appointment with ID: {AppointmentId} successfully confirmed.", appointmentId); + + return ResultT.Success(confirmAppointmentDTos); + } +} \ No newline at end of file diff --git a/Trimly.Core.Application/Services/CodeConfirmationService.cs b/Trimly.Core.Application/Services/CodeConfirmationService.cs new file mode 100644 index 0000000..3f095fd --- /dev/null +++ b/Trimly.Core.Application/Services/CodeConfirmationService.cs @@ -0,0 +1,63 @@ +using System.Collections.Concurrent; +using System.Security.Cryptography; +using System.Text; +using Trimly.Core.Application.DTOs.Email; +using Trimly.Core.Application.Interfaces.Service; + +namespace Trimly.Core.Application.Services; + +public class CodeConfirmationService : ICodeConfirmationService +{ + private static readonly ConcurrentDictionary _generatedCodes = new(); + private readonly IEmailService _emailService; + + public CodeConfirmationService(IEmailService emailService) + { + _emailService = emailService; + } + + public async Task GenerateCodeConfirmation(string emailAddress) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + int length = RandomNumberGenerator.GetInt32(8, 16); + StringBuilder stringBuilder = new(length); + + for (int i = 0; i < length; i++) + { + stringBuilder.Append(chars[RandomNumberGenerator.GetInt32(chars.Length)]); + } + + string randomString = stringBuilder.ToString(); + + // Garantizar unicidad por usuario + _generatedCodes[emailAddress] = randomString; + + await _emailService.SenAsync(new EmailRequestDTos + { + To = emailAddress, + Body = $@" +
+

Confirm Your Account Registration

+

+ Hello {emailAddress},
+ Thank you for registering with us. To complete your account setup, please use the verification token provided below. +

+
+ Your Verification Token: +

{randomString}

+
+

+ If you didn’t request this email, no further action is required. Please feel free to contact us if you have any concerns. +

+
+

+ This email is brought to you by Caring for Paws.
+ Please do not reply directly to this email as it is not monitored. +

+
", + Subject = "Confirmation Code" + }); + + return randomString; + } +} \ No newline at end of file diff --git a/Trimly.Core.Application/Services/RegisteredCompaniesService.cs b/Trimly.Core.Application/Services/RegisteredCompaniesService.cs new file mode 100644 index 0000000..a3b6cd8 --- /dev/null +++ b/Trimly.Core.Application/Services/RegisteredCompaniesService.cs @@ -0,0 +1,402 @@ +using Microsoft.Extensions.Logging; +using Trimly.Core.Application.DTOs.RegisteredCompanies; +using Trimly.Core.Application.Interfaces.Repository; +using Trimly.Core.Application.Interfaces.Service; +using Trimly.Core.Application.Pagination; +using Trimly.Core.Domain.Enum; +using Trimly.Core.Domain.Models; +using Trimly.Core.Domain.Utils; + +namespace Trimly.Core.Application.Services; + +public class RegisteredCompaniesService : IRegisteredCompaniesService +{ + private readonly IRegisteredCompanyRepository _registeredCompanyRepository; + private readonly ILogger _logger; + private readonly ICloudinaryService _cloudinaryService; + public RegisteredCompaniesService(IRegisteredCompanyRepository registeredCompanyRepository, ILogger logger, ICloudinaryService cloudinaryService) + { + _registeredCompanyRepository = registeredCompanyRepository; + _logger = logger; + _cloudinaryService = cloudinaryService; + } + + public async Task>> GetPagedResult(int pageNumber, int pageSize, CancellationToken cancellationToken) + { + if (pageNumber <= 0 && pageSize <= 0) + { + _logger.LogError("Invalid pagination parameters: pageNumber and pageSize must be greater than 1."); + + return ResultT>.Failure( + Error.Failure("400", "Invalid pagination parameters. Both pageNumber and pageSize must be greater than 1.") + ); + } + var registeredCompanyWithNumber = await _registeredCompanyRepository.GetPagedResultAsync(pageNumber, pageSize, cancellationToken); + var dtoItems = registeredCompanyWithNumber.Items.Select(x => new RegisteredCompaniesDTos + ( + RegisteredCompaniesId: x.RegisteredCompaniesId, + Name: x.Name, + PhoneNumber: x.Phone, + AddresCompanies: x.Address, + DescriptionCompanies: x.Description, + LogoUrl: x.LogoUrl, + Status: x.Status, + RegistrationDateCompany: x.RegistrationDate + )).ToList(); + + if (!dtoItems.Any()) + { + _logger.LogError("No register companies found"); + + return ResultT>.Failure(Error.Failure("404", "The list is empty")); + } + + PagedResult pagedResult = new() + { + TotalItems = registeredCompanyWithNumber.TotalItems, + CurrentPage = registeredCompanyWithNumber.CurrentPage, + TotalPages = registeredCompanyWithNumber.TotalPages, + Items = dtoItems + }; + + _logger.LogInformation("Successfully retrieved {Count} registered companies. Page {CurrentPage} of {TotalPages}.", + dtoItems.Count, pagedResult.CurrentPage, pagedResult.TotalPages); + + return ResultT>.Success(pagedResult); + } + + public async Task> GetByIdAsync(Guid id, CancellationToken cancellationToken) + { + var registeredCompany = await _registeredCompanyRepository.GetByIdAsync(id, cancellationToken); + if (registeredCompany == null) + { + _logger.LogError("No register companies found"); + + return ResultT.Failure(Error.Failure("404", "The register companies could not be found")); + } + + RegisteredCompaniesDTos registeredCompaniesDTos = new + ( + RegisteredCompaniesId: registeredCompany.RegisteredCompaniesId, + Name: registeredCompany.Name, + PhoneNumber: registeredCompany.Phone, + AddresCompanies: registeredCompany.Address, + DescriptionCompanies: registeredCompany.Description, + LogoUrl: registeredCompany.LogoUrl, + Status: registeredCompany.Status, + RegistrationDateCompany: registeredCompany.RegistrationDate + ); + + _logger.LogInformation("Register companies found"); + + return ResultT.Success(registeredCompaniesDTos); + } + + public async Task> CreateAsync(CreateRegisteredCompaniesDTos createAppointmentDTos, CancellationToken cancellationToken) + { + if (createAppointmentDTos == null) + { + _logger.LogError("Invalid parameter: createRegisteredCompaniesDTos"); + + return ResultT.Failure(Error.Failure("400", "Invalid parameter: createRegisteredCompaniesDTos")); + } + + var exists = await _registeredCompanyRepository.ValidateAsync(x => x.Name == createAppointmentDTos.Name); + if (exists) + { + _logger.LogError("Company registration failed: A company with the name '{CompanyName}' already exists.", createAppointmentDTos.Name); + + return ResultT.Failure( + Error.Failure("400", $"A company with the name '{createAppointmentDTos.Name}' already exists. Please choose a different name.") + ); + } + + string? logoUrl = null; + if (createAppointmentDTos.ImageFile != null) + { + using var stream = createAppointmentDTos.ImageFile.OpenReadStream(); + logoUrl = await _cloudinaryService.UploadImageCloudinaryAsync( + stream, + createAppointmentDTos.ImageFile.FileName, + cancellationToken); + } + + Domain.Models.RegisteredCompanies registeredCompany = new() + { + RegisteredCompaniesId = Guid.NewGuid(), + Name = createAppointmentDTos.Name, + RNC = createAppointmentDTos.Rnc, + Phone = createAppointmentDTos.PhoneNumber, + Address = createAppointmentDTos.AddresCompanies, + Email = createAppointmentDTos.Email, + Description = createAppointmentDTos.DescriptionCompanies, + LogoUrl = logoUrl, + Status = Domain.Enum.Status.Activated, + }; + + await _registeredCompanyRepository.AddAsync(registeredCompany, cancellationToken); + + RegisteredCompaniesDTos companiesDTos = new + ( + RegisteredCompaniesId: registeredCompany.RegisteredCompaniesId, + Name: registeredCompany.Name, + PhoneNumber: registeredCompany.Phone, + AddresCompanies: registeredCompany.Address, + DescriptionCompanies: registeredCompany.Description, + LogoUrl: logoUrl, + Status: registeredCompany.Status, + RegistrationDateCompany: registeredCompany.RegistrationDate + ); + + _logger.LogInformation("Company '{CompanyName}' has been successfully registered with ID {CompanyId}.", + companiesDTos.Name, companiesDTos.RegisteredCompaniesId); + + return ResultT.Success(companiesDTos); + } + + public async Task> UpdateAsync(Guid id, RegisteredCompaniesUpdateDTos entity, CancellationToken cancellation) + { + var registeredCompany = await _registeredCompanyRepository.GetByIdAsync(id, cancellation); + if (registeredCompany == null) + { + _logger.LogError("No register companies found"); + + return ResultT.Failure(Error.Failure("404", $"{id} is not registered")); + } + + var existsName = await _registeredCompanyRepository.ValidateAsync(x => x.Name == entity.Name); + if (existsName) + { + _logger.LogError("Company registration failed: Name already exists."); + + return ResultT.Failure(Error.Failure("400", $"{id} is already registered")); + } + + registeredCompany.Name = entity.Name; + registeredCompany.Phone = entity.Phone; + registeredCompany.Email = entity.Email; + registeredCompany.Description = entity.DescriptionCompanies; + registeredCompany.LogoUrl = entity.LogoUrl; + registeredCompany.Address = entity.AddressCompany; + registeredCompany.Status = entity.Status; + + await _registeredCompanyRepository.UpdateAsync(registeredCompany, cancellation); + + RegisteredCompaniesDTos companiesDTos = new + ( + RegisteredCompaniesId: registeredCompany.RegisteredCompaniesId, + Name: registeredCompany.Name, + PhoneNumber: registeredCompany.Phone, + AddresCompanies: registeredCompany.Address, + DescriptionCompanies: registeredCompany.Description, + LogoUrl: registeredCompany.LogoUrl, + Status: registeredCompany.Status, + RegistrationDateCompany: registeredCompany.RegistrationDate + ); + + _logger.LogInformation("Company '{CompanyName}' (ID: {CompanyId}) has been successfully updated.", + registeredCompany.Name, registeredCompany.RegisteredCompaniesId); + + return ResultT.Success(companiesDTos); + } + + public async Task> DeleteAsync(Guid id, CancellationToken cancellation) + { + var registeredCompany = await _registeredCompanyRepository.GetByIdAsync(id, cancellation); + if (registeredCompany == null) + { + _logger.LogError("No register companies found"); + + return ResultT.Failure(Error.Failure("404", $"{id} is not registered")); + } + + await _registeredCompanyRepository.DeleteAsync(registeredCompany, cancellation); + + _logger.LogInformation("Company with ID {CompanyId} has been successfully deleted.", id); + + return ResultT.Success(registeredCompany.RegisteredCompaniesId ?? Guid.Empty); + } + + public async Task>> FilterByNameAsync(string name, CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(name)) + { + _logger.LogError("Validation failed: The 'name' parameter is missing or empty."); + + return ResultT>.Failure( + Error.Failure("400", "The 'name' field is required and cannot be empty.") + ); + } + + var exists = await _registeredCompanyRepository.ValidateAsync(x => x.Name == name); + if (!exists) + { + _logger.LogError("Validation failed: The company '{name}' is not registered.",name); + + return ResultT>.Failure( + Error.Failure("404", $"The company '{name}' is not registered.") + ); + } + + var filterByName = await _registeredCompanyRepository.OrderByNameAsync(cancellationToken); + if (filterByName == null || !filterByName.Any()) + { + _logger.LogWarning("No registered companies found."); + + return ResultT>.Failure( + Error.Failure("404", "No registered companies found.") + ); + } + + IEnumerable comapanyDTos = + filterByName.Select(x => new OrderNameComapanyDTos + ( + NameRegistedCompanyDTos: x.Name + )); + + _logger.LogInformation("Successfully retrieved {Count} registered companies matching the name '{CompanyName}'.", + comapanyDTos.Count(), name); + + return ResultT>.Success(comapanyDTos); + } + + public async Task>> FilterByStatusAsync(Status status, CancellationToken cancellationToken) + { + var exists = await _registeredCompanyRepository.ValidateAsync(x => x.Status == status); + if (!exists) + { + _logger.LogError("No registered companies found with status: {Status}", status); + + return ResultT>.Failure( + Error.NotFound("404", $"No companies found with status '{status}'.") + ); + } + + var filterByStatus = await _registeredCompanyRepository.FilterByStatusAsync(status, cancellationToken); + if (filterByStatus == null || !filterByStatus.Any()) + { + _logger.LogWarning("Filter result is empty for status: {Status}", status); + + return ResultT>.Failure( + Error.NotFound("404", $"No companies found with status '{status}'.") + ); + } + + IEnumerable comapanyDTos = filterByStatus.Select(x => new RegisteredCompaniesDTos + ( + RegisteredCompaniesId: x.RegisteredCompaniesId, + Name: x.Name, + PhoneNumber: x.Phone, + AddresCompanies: x.Address, + DescriptionCompanies: x.Description, + LogoUrl: x.LogoUrl, + Status: x.Status, + RegistrationDateCompany: x.RegistrationDate + )); + + _logger.LogInformation("Successfully retrieved {Count} registered companies.", comapanyDTos.Count()); + + return ResultT>.Success(comapanyDTos); + } + + + public async Task>> OrderByNameAsync(CancellationToken cancellationToken) + { + var registeredCompaniesOrderNames = await _registeredCompanyRepository.OrderByNameAsync(cancellationToken); + if (!registeredCompaniesOrderNames.Any()) + { + _logger.LogWarning("No registered companies found."); + + return ResultT>.Failure(Error.NotFound("404", + $"No registered companies found.")); + } + + IEnumerable companiesDTos = registeredCompaniesOrderNames.Select(x => new RegisteredCompaniesDTos + ( + RegisteredCompaniesId: x.RegisteredCompaniesId, + Name: x.Name, + PhoneNumber: x.Phone, + AddresCompanies: x.Address, + DescriptionCompanies: x.Description, + LogoUrl: x.LogoUrl, + Status: x.Status, + RegistrationDateCompany: x.RegistrationDate + )); + + _logger.LogInformation("Successfully retrieved {Count} registered companies ordered by name.", companiesDTos.Count()); + + return ResultT>.Success(companiesDTos); + } + + public async Task>> GetRecentAsync(CancellationToken cancellationToken) + { + var registeredCompaniesRecent = await _registeredCompanyRepository.GetRecentAsync(cancellationToken); + if ( registeredCompaniesRecent == null || !registeredCompaniesRecent.Any()) + { + _logger.LogError("No recent registered companies found."); + + return ResultT>.Failure(Error.NotFound("404", "The list is empty")); + } + + IEnumerable companiesDTos = registeredCompaniesRecent.Select(x => new RegisteredCompaniesDTos + ( + RegisteredCompaniesId: x.RegisteredCompaniesId, + Name: x.Name, + PhoneNumber: x.Phone, + AddresCompanies: x.Address, + DescriptionCompanies: x.Description, + LogoUrl: x.LogoUrl, + Status: x.Status, + RegistrationDateCompany: x.RegistrationDate + )); + + _logger.LogInformation("Successfully retrieved {Count} recent registered companies.", companiesDTos.Count()); + + return ResultT>.Success(companiesDTos); + } + + public async Task>> OrderByIdAsync(string order, CancellationToken cancellationToken) + { + var companyKey = GetSort(); + if (companyKey.TryGetValue((order), out var companyOrder)) + { + var orderId = await companyOrder(cancellationToken); + if (orderId == null || !orderId.Any()) + { + _logger.LogError("Attempted to fetch registered companies, but the provided order ID list was null or empty."); + + return ResultT>.Failure(Error.NotFound("404", "The list is empty")); + } + + IEnumerable companiesDTos = orderId.Select(x => new RegisteredCompaniesDTos + ( + RegisteredCompaniesId: x.RegisteredCompaniesId, + Name: x.Name, + PhoneNumber: x.Phone, + AddresCompanies: x.Address, + DescriptionCompanies: x.Description, + LogoUrl: x.LogoUrl, + Status: x.Status, + RegistrationDateCompany: x.RegistrationDate + )); + + _logger.LogInformation("Registered companies data fetched successfully. Returning {Count} results.", companiesDTos.Count()); + + return ResultT>.Success(companiesDTos); + } + _logger.LogError("Invalid order key '{Order}'. No matching sorting function found.", order); + + return ResultT>.Failure(Error.NotFound("404", "No registered companies found.")); + } + + #region Private methods + private Dictionary>>> GetSort() + { + return new Dictionary>>>() + { + {"asc", async cancellationToken => await _registeredCompanyRepository.OrderByIdAscAsync(cancellationToken)}, + {"desc", async cancellationToken => await _registeredCompanyRepository.OrderByIdDescAsync(cancellationToken)} + }; + } + #endregion +} \ No newline at end of file diff --git a/Trimly.Core.Application/Services/ReviewService.cs b/Trimly.Core.Application/Services/ReviewService.cs new file mode 100644 index 0000000..9562f20 --- /dev/null +++ b/Trimly.Core.Application/Services/ReviewService.cs @@ -0,0 +1,186 @@ +using Microsoft.Extensions.Logging; +using Trimly.Core.Application.DTOs.Review; +using Trimly.Core.Application.Interfaces.Repository; +using Trimly.Core.Application.Interfaces.Service; +using Trimly.Core.Application.Pagination; +using Trimly.Core.Domain.Utils; + +namespace Trimly.Core.Application.Services; + +public class ReviewService : IReviewService +{ + private readonly IReviewsRepository _reviewsRepository; + private readonly ILogger _logger; + private readonly IRegisteredCompanyRepository _registeredCompanyRepository; + + public ReviewService(IReviewsRepository reviewsRepository, ILogger logger, IRegisteredCompanyRepository registeredCompanyRepository) + { + _reviewsRepository = reviewsRepository; + _logger = logger; + _registeredCompanyRepository = registeredCompanyRepository; + } + + public async Task>> GetPagedResult(int pageNumber, int pageSize, CancellationToken cancellationToken) + { + if (pageNumber <= 0 && pageSize <= 0) + { + _logger.LogWarning("Invalid pagination parameters: pageNumber and pageSize must be greater than zero."); + + return ResultT>.Failure(Error.Failure("400", "Both pageNumber and pageSize must be greater than zero.")); + } + + var reviewNumber = await _reviewsRepository.GetPagedResultAsync(pageNumber, pageSize, cancellationToken); + IEnumerable reviewsPagedItems = reviewNumber.Items.Select(x => new ReviewsDTos + ( + ReviewId: x.ReviewId, + Title: x.Title, + Comment: x.Comment, + Rating: x.Rating, + RegisteredCompanyId: x.RegisteredCompanyId, + CreatedAt: x.CreatedAt, + UpdateAt: x.UpdateAt + )); + + PagedResult pagedResult = new PagedResult() + { + TotalItems = reviewNumber.TotalItems, + Items = reviewsPagedItems, + TotalPages = reviewNumber.TotalPages, + CurrentPage = reviewNumber.CurrentPage + }; + + _logger.LogInformation("Successfully retrieved paginated reviews: Page {CurrentPage} of {TotalPages}, TotalItems: {TotalItems}", + pagedResult.CurrentPage, pagedResult.TotalPages, pagedResult.TotalItems); + + return ResultT>.Success(pagedResult); + } + + public async Task> GetByIdAsync(Guid id, CancellationToken cancellationToken) + { + var review = await _reviewsRepository.GetByIdAsync(id, cancellationToken); + if (review == null) + { + _logger.LogWarning("Review with ID {ReviewId} was not found.", id); + + return ResultT.Failure(Error.NotFound("404", $"Review with ID {id} was not found.")); + } + + ReviewsDTos reviewsDTos = new + ( + ReviewId: review.ReviewId, + Title: review.Title, + Comment: review.Comment, + Rating: review.Rating, + RegisteredCompanyId: review.RegisteredCompanyId, + CreatedAt: review.CreatedAt, + UpdateAt: review.UpdateAt + ); + + _logger.LogInformation("Review with ID {ReviewId} was successfully retrieved.", id); + + return ResultT.Success(reviewsDTos); + } + + public async Task> CreateAsync(CreateReviewsDTos createAppointmentDTos, CancellationToken cancellationToken) + { + if (createAppointmentDTos == null) + { + _logger.LogWarning("The request to create a review is null."); + + return ResultT.Failure(Error.Failure("400", "Review data is required.")); + } + + Domain.Models.Reviews reviews = new() + { + ReviewId = Guid.NewGuid(), + Title = createAppointmentDTos.Title, + Comment = createAppointmentDTos.Comment, + Rating = createAppointmentDTos.Rating, + RegisteredCompanyId = createAppointmentDTos.RegisteredCompanyId, + UpdateAt = DateTime.UtcNow + }; + + await _reviewsRepository.AddAsync(reviews, cancellationToken); + + _logger.LogInformation("Review with ID {ReviewId} has been successfully created.", reviews.ReviewId); + + ReviewsDTos reviewsDTos = new + ( + ReviewId: reviews.ReviewId, + Title: reviews.Title, + Comment: reviews.Comment, + Rating: reviews.Rating, + RegisteredCompanyId: reviews.RegisteredCompanyId, + CreatedAt: reviews.CreatedAt, + UpdateAt: reviews.UpdateAt + ); + + return ResultT.Success(reviewsDTos); + } + + public async Task> UpdateAsync(Guid id, ReviewsUpdateDTos entity, CancellationToken cancellation) + { + var review = await _reviewsRepository.GetByIdAsync(id, cancellation); + if (review == null) + { + _logger.LogError("Review with ID {ReviewId} was not found for update.", id); + + return ResultT.Failure(Error.NotFound("404", $"Review with ID {id} was not found.")); + } + + review.Title = entity.Title; + review.Comment = entity.Comment; + review.Rating = entity.Rating; + + await _reviewsRepository.UpdateAsync(review, cancellation); + + _logger.LogInformation("Review with ID {ReviewId} has been successfully updated.", id); + + ReviewsDTos reviewsDTos = new + ( + ReviewId: review.ReviewId, + Title: review.Title, + Comment: review.Comment, + Rating: review.Rating, + RegisteredCompanyId: review.RegisteredCompanyId, + CreatedAt: review.CreatedAt, + UpdateAt: review.UpdateAt + ); + + return ResultT.Success(reviewsDTos); + } + + public async Task> DeleteAsync(Guid id, CancellationToken cancellation) + { + var review = await _reviewsRepository.GetByIdAsync(id, cancellation); + if (review == null) + { + _logger.LogError("Review with ID {ReviewId} was not found for deletion.", id); + + return ResultT.Failure(Error.NotFound("404", $"Review with ID {id} was not found.")); + } + + await _reviewsRepository.DeleteAsync(review, cancellation); + + _logger.LogInformation("Review with ID {ReviewId} has been successfully deleted.", id); + + return ResultT.Success(review.ReviewId ?? Guid.Empty); + } + + public async Task> GetAverageRatingAsync(Guid companyId, CancellationToken cancellationToken) + { + var registeredCompany = await _registeredCompanyRepository.GetByIdAsync(companyId, cancellationToken); + if (registeredCompany == null) + { + _logger.LogError("Company with ID {CompanyId} was not found.", companyId); + + return ResultT.Failure(Error.NotFound("404", $"Company with ID {companyId} was not found.")); + } + + var result = await _reviewsRepository.GetAverageRatingAsync(companyId, cancellationToken); + + _logger.LogInformation("Retrieved average rating for company with ID {CompanyId}.", companyId); + + return ResultT.Success(result); + } +} \ No newline at end of file diff --git a/Trimly.Core.Application/Services/SchedulesService.cs b/Trimly.Core.Application/Services/SchedulesService.cs new file mode 100644 index 0000000..19fc5f7 --- /dev/null +++ b/Trimly.Core.Application/Services/SchedulesService.cs @@ -0,0 +1,357 @@ +using System.Collections; +using Microsoft.AspNetCore.Mvc.RazorPages; +using Microsoft.Extensions.Logging; +using Trimly.Core.Application.DTOs.Schedules; +using Trimly.Core.Application.Interfaces.Repository; +using Trimly.Core.Application.Interfaces.Service; +using Trimly.Core.Application.Pagination; +using Trimly.Core.Domain.Enum; +using Trimly.Core.Domain.Models; +using Trimly.Core.Domain.Utils; + +namespace Trimly.Core.Application.Services; + +public class SchedulesService : ISchedulesService +{ + private readonly ISchedulesRepository _repository; + private readonly ILogger _logger; + private readonly IRegisteredCompanyRepository _registeredCompaniesRepository; + public SchedulesService(ISchedulesRepository repository, ILogger logger, IRegisteredCompanyRepository registeredCompaniesRepository) + { + _repository = repository; + _logger = logger; + _registeredCompaniesRepository = registeredCompaniesRepository; + } + public async Task>> GetPagedResult(int pageNumber, int pageSize, CancellationToken cancellationToken) + { + if (pageNumber <= 0 || pageSize <= 0) + { + _logger.LogWarning("Invalid pagination parameters: PageNumber ({0}) and PageSize ({1}) must be greater than zero.", pageNumber, pageSize); + + return ResultT>.Failure(Error.Failure("400", "Invalid pagination parameters. Page number and page size must be greater than zero.")); + } + + var schedulesNumber = await _repository.GetPagedResultAsync(pageNumber, pageSize, cancellationToken); + IEnumerable schedulesDtos = schedulesNumber.Items.Select(x => new SchedulesDTos + ( + SchedulesId: x.SchedulesId, + Days: x.Week, + OpeningTime: x.OpeningTime, + ClosingTime: x.ClosingTime, + Notes: x.Notes, + IsHoliday: x.IsHoliday, + RegisteredCompanyId: x.RegisteredCompanyId, + CreatedAt: x.CreatedAt, + UpdateAt: DateTime.UtcNow + )); + + if (!schedulesDtos.Any()) + { + _logger.LogWarning("No schedules found for the requested pagination parameters: PageNumber ({0}), PageSize ({1}).", pageNumber, pageSize); + + return ResultT>.Failure(Error.Failure("404", "No schedules found for the given page number and page size.")); + } + + PagedResult pagedResult = new() + { + Items = schedulesDtos, + CurrentPage = schedulesNumber.CurrentPage, + TotalItems = schedulesNumber.TotalItems, + TotalPages = schedulesNumber.TotalPages + }; + + _logger.LogInformation("Successfully retrieved {0} schedule(s) for Page {1} with Page Size {2}.", schedulesDtos.Count(), pageNumber, pageSize); + + return ResultT>.Success(pagedResult); + } + + public async Task> GetByIdAsync(Guid id, CancellationToken cancellationToken) + { + var schedule = await _repository.GetByIdAsync(id, cancellationToken); + if (schedule == null) + { + _logger.LogWarning("Schedule with ID {0} was not found.", id); + + return ResultT.Failure(Error.Failure("404", $"Schedule with ID {id} was not found.")); + } + + SchedulesDTos schedulesDTos = new + ( + SchedulesId: schedule.SchedulesId, + Days: schedule.Week, + OpeningTime: schedule.OpeningTime, + ClosingTime: schedule.ClosingTime, + Notes: schedule.Notes, + IsHoliday: schedule.IsHoliday, + RegisteredCompanyId: schedule.RegisteredCompanyId, + CreatedAt: schedule.CreatedAt, + UpdateAt: DateTime.UtcNow + ); + + _logger.LogInformation("Successfully retrieved schedule with ID {0}.", id); + + return ResultT.Success(schedulesDTos); + } + + public async Task> CreateAsync(CreateSchedulesDTos entityCreateDTo, CancellationToken cancellationToken) + { + if (entityCreateDTo == null) + { + _logger.LogError(""); + + return ResultT.Failure(Error.Failure("400", "CreateReviewsDTos parameters are required.")); + } + + Domain.Models.Schedules schedules = new() + { + SchedulesId = Guid.NewGuid(), + Week = entityCreateDTo.Days, + OpeningTime = entityCreateDTo.OpeningTime, + ClosingTime = entityCreateDTo.ClosingTime, + Notes = entityCreateDTo.Notes, + IsHoliday = entityCreateDTo.IsHoliday, + RegisteredCompanyId = entityCreateDTo.RegisteredCompanyId + }; + + await _repository.AddAsync(schedules, cancellationToken); + + SchedulesDTos schedulesDTos = new + ( + SchedulesId: schedules.SchedulesId, + Days: schedules.Week, + OpeningTime: schedules.OpeningTime, + ClosingTime: schedules.ClosingTime, + Notes: schedules.Notes, + IsHoliday: schedules.IsHoliday, + RegisteredCompanyId: schedules.RegisteredCompanyId, + CreatedAt: schedules.CreatedAt, + UpdateAt: DateTime.UtcNow + ); + + _logger.LogInformation(""); + + return ResultT.Success(schedulesDTos); + } + + public async Task> UpdateAsync(Guid id, UpdateSchedulesDTos entity, CancellationToken cancellation) + { + var schedule = await _repository.GetByIdAsync(id, cancellation); + if (schedule == null) + { + _logger.LogError(""); + + return ResultT.Failure(Error.Failure("404", "Schedule with ID {0} was not found.")); + } + + schedule.Week = entity.Days; + schedule.OpeningTime = entity.OpeningTime; + schedule.ClosingTime = entity.ClosingTime; + schedule.Notes = entity.Notes; + schedule.IsHoliday = entity.IsHoliday; + + await _repository.UpdateAsync(schedule, cancellation); + + SchedulesDTos schedulesDTos = new + ( + SchedulesId: schedule.SchedulesId, + Days: schedule.Week, + OpeningTime: schedule.OpeningTime, + ClosingTime: schedule.ClosingTime, + Notes: schedule.Notes, + IsHoliday: schedule.IsHoliday, + RegisteredCompanyId: schedule.RegisteredCompanyId, + CreatedAt: schedule.CreatedAt, + UpdateAt: DateTime.UtcNow + ); + + _logger.LogInformation(""); + + return ResultT.Success(schedulesDTos); + } + + public async Task> DeleteAsync(Guid id, CancellationToken cancellation) + { + var schedule = await _repository.GetByIdAsync(id, cancellation); + if (schedule == null) + { + _logger.LogError(""); + + return ResultT.Failure(Error.NotFound("404", "Schedule with ID {0} was not found.")); + } + + await _repository.DeleteAsync(schedule, cancellation); + + _logger.LogInformation(""); + + return ResultT.Success(schedule.SchedulesId ?? Guid.Empty); + } + public async Task> ActivatedIsHolidayAsync(Guid registeredCompany, CancellationToken cancellationToken) + { + var schedules = await _repository.GetScheduleByCompanyIdAsync(registeredCompany, cancellationToken); + if (schedules == null) + { + _logger.LogError("Company with ID {CompanyId} was not found.", registeredCompany); + return ResultT.Failure(Error.NotFound("404", $"Company with ID {registeredCompany} was not found.")); + } + + schedules.IsHoliday = Status.Activated; + + await _repository.UpdateAsync(schedules, cancellationToken); + + _logger.LogInformation("Holiday status for Company ID {CompanyId} has been successfully activated.", registeredCompany); + + return ResultT.Success("The holiday status has been successfully activated."); + } + + public async Task>> FilterByOpeningTimeAsync(Guid registeredCompany, TimeOnly openingTime, CancellationToken cancellationToken) + { + var registeredCompanies = await _registeredCompaniesRepository.GetByIdAsync(registeredCompany, cancellationToken); + if (registeredCompanies == null) + { + _logger.LogError(""); + + return ResultT>.Failure(Error.NotFound("404", "Schedule with ID {0} was not found.")); + } + + var schedules = await _repository.FilterByOpeningTimeAsync(openingTime, cancellationToken); + if (schedules == null || !schedules.Any()) + { + _logger.LogError(""); + + return ResultT>.Failure(Error.Failure("400", "Schedule with ID {0} was not found.")); + } + + IEnumerable schedulesDTos = schedules.Select(x => new SchedulesDTos + ( + SchedulesId: x.SchedulesId, + Days: x.Week, + OpeningTime: x.OpeningTime, + ClosingTime: x.ClosingTime, + Notes: x.Notes, + IsHoliday: x.IsHoliday, + RegisteredCompanyId: x.RegisteredCompanyId, + CreatedAt: x.CreatedAt, + UpdateAt: DateTime.UtcNow + )); + + _logger.LogInformation(""); + + return ResultT>.Success(schedulesDTos); + } + + public async Task>> FilterByIsHolidayAsync(Guid registeredCompany, Status isHoliday, CancellationToken cancellationToken) + { + var registeredCompanies = await _registeredCompaniesRepository.GetByIdAsync(registeredCompany, cancellationToken); + if (registeredCompanies == null) + { + _logger.LogError(""); + + return ResultT>.Failure(Error.NotFound("404", "Schedule with ID {0} was not found.")); + } + + var schedulesIsHoliday = await _repository.FilterByIsHolidayAsync(registeredCompany, isHoliday ,cancellationToken); + if (schedulesIsHoliday == null || !schedulesIsHoliday.Any()) + { + _logger.LogError(""); + + return ResultT>.Failure(Error.Failure("400", "Schedule with ID {0} was not found.")); + } + + IEnumerable schedulesDTos = schedulesIsHoliday.Select(x => new SchedulesDTos + ( + SchedulesId: x.SchedulesId, + Days: x.Week, + OpeningTime: x.OpeningTime, + ClosingTime: x.ClosingTime, + Notes: x.Notes, + IsHoliday: x.IsHoliday, + RegisteredCompanyId: x.RegisteredCompanyId, + CreatedAt: x.CreatedAt, + UpdateAt: DateTime.UtcNow + )); + + _logger.LogInformation(""); + + return ResultT>.Success(schedulesDTos); + } + + public async Task>> FilterByWeekDayAsync(Guid registeredCompany, Weekday weekday, CancellationToken cancellationToken) + { + var registeredCompanies = await _registeredCompaniesRepository.GetByIdAsync(registeredCompany, cancellationToken); + if (registeredCompanies == null) + { + _logger.LogError(""); + + return ResultT>.Failure(Error.NotFound("404", "Schedule with ID {0} was not found.")); + } + + var existsWeekDay = await _repository.ValidateAsync(x => x.Week == weekday); + if (!existsWeekDay) + { + _logger.LogError(""); + + return ResultT>.Failure(Error.NotFound("404", "Schedule with ID {0} was not found.")); + } + + var schedulesByWeekDay = await _repository.FilterByWeekDayAsync(weekday, cancellationToken); + if (schedulesByWeekDay == null || !schedulesByWeekDay.Any()) + { + _logger.LogError(""); + + return ResultT>.Failure(Error.Failure("400", "Schedule with ID {0} was not found.")); + } + + IEnumerable schedulesDTos = schedulesByWeekDay.Select(x => new SchedulesDTos + ( + SchedulesId: x.SchedulesId, + Days: x.Week, + OpeningTime: x.OpeningTime, + ClosingTime: x.ClosingTime, + Notes: x.Notes, + IsHoliday: x.IsHoliday, + RegisteredCompanyId: x.RegisteredCompanyId, + CreatedAt: x.CreatedAt, + UpdateAt: DateTime.UtcNow + )); + + _logger.LogInformation(""); + + return ResultT>.Success(schedulesDTos); + } + + public async Task>> GetSchedulesByCompanyIdAsync(Guid registeredCompanyId, CancellationToken cancellationToken) + { + var registeredCompanies = await _registeredCompaniesRepository.GetByIdAsync(registeredCompanyId, cancellationToken); + if (registeredCompanies == null) + { + _logger.LogError(""); + + return ResultT>.Failure(Error.NotFound("404", "Schedule with ID {0} was not found.")); + } + + var schedulesByCompany = await _repository.GetSchedulesByCompanyId(registeredCompanyId, cancellationToken); + if (schedulesByCompany == null || !schedulesByCompany.Any()) + { + _logger.LogError(""); + + return ResultT>.Failure(Error.Failure("400", "Schedule with ID {0} was not found.")); + } + + IEnumerable schedulesDTos = schedulesByCompany.Select(x => new SchedulesDTos + ( + SchedulesId: x.SchedulesId, + Days: x.Week, + OpeningTime: x.OpeningTime, + ClosingTime: x.ClosingTime, + Notes: x.Notes, + IsHoliday: x.IsHoliday, + RegisteredCompanyId: x.RegisteredCompanyId, + CreatedAt: x.CreatedAt, + UpdateAt: DateTime.UtcNow + )); + + _logger.LogInformation(""); + + return ResultT>.Success(schedulesDTos); + } +} \ No newline at end of file diff --git a/Trimly.Core.Application/Services/ServicesService.cs b/Trimly.Core.Application/Services/ServicesService.cs new file mode 100644 index 0000000..5733e05 --- /dev/null +++ b/Trimly.Core.Application/Services/ServicesService.cs @@ -0,0 +1,381 @@ +using Microsoft.Extensions.Logging; +using Trimly.Core.Application.DTOs.Service; +using Trimly.Core.Application.Interfaces.Repository; +using Trimly.Core.Application.Interfaces.Service; +using Trimly.Core.Application.Pagination; +using Trimly.Core.Domain.Enum; +using Trimly.Core.Domain.Utils; + +namespace Trimly.Core.Application.Services; + +public class ServicesService : IServicesService +{ + private readonly IServiceRepository _repository; + private readonly ILogger _logger; + private readonly ICloudinaryService _cloudinaryService; + private readonly IRegisteredCompanyRepository _registeredCompanyRepository; + + public ServicesService(IServiceRepository repository, ILogger logger, ICloudinaryService cloudinaryService, IRegisteredCompanyRepository registeredCompanyRepository) + { + _repository = repository; + _logger = logger; + _cloudinaryService = cloudinaryService; + _registeredCompanyRepository = registeredCompanyRepository; + } + + public async Task>> GetPagedResult(int pageNumber, int pageSize, CancellationToken cancellationToken) + { + if (pageNumber <= 0 || pageSize <= 0) + { + _logger.LogError("Invalid pagination parameters. PageNumber: {PageNumber}, PageSize: {PageSize}.", pageNumber, pageSize); + + return ResultT>.Failure(Error.Failure("400", "Page number and page size must be greater than zero.")); + } + + var servicesNumber = await _repository.GetPagedResultAsync(pageNumber, pageSize, cancellationToken); + IEnumerable servicesDTos = servicesNumber.Items.Select(x => new ServicesDTos + ( + ServiceId: x.ServicesId, + Name: x.Name, + Price: x.Price, + Description: x.Description, + DurationInMinutes: x.DurationInMinutes, + ImageUrl: x.ImageUrl, + Status: x.Status, + RegisteredCompanyId: x.RegisteredCompanyId, + CreatedAt: x.CreatedAt, + UpdateAt: x.UpdateAt + )); + + if ( servicesDTos == null || !servicesDTos.Any()) + { + _logger.LogError("No services found for the given pagination parameters. PageNumber: {PageNumber}, PageSize: {PageSize}.", + pageNumber, pageSize); + + return ResultT>.Failure(Error.Failure("400", "No services available for the requested page.")); + } + + PagedResult pagedResult = new() + { + TotalItems = servicesNumber.TotalItems, + CurrentPage = servicesNumber.CurrentPage, + TotalPages = servicesNumber.TotalPages, + Items = servicesDTos + }; + + _logger.LogInformation("Successfully retrieved {TotalItems} services. Page {CurrentPage} of {TotalPages}.", + servicesNumber.TotalItems, servicesNumber.CurrentPage, servicesNumber.TotalPages); + + return ResultT>.Success(pagedResult); + } + + public async Task> GetByIdAsync(Guid id, CancellationToken cancellationToken) + { + var service = await _repository.GetByIdAsync(id, cancellationToken); + if (service == null) + { + _logger.LogError("{id} not found in services",id); + + return ResultT.Failure(Error.Failure("404", $"{id} Service not found.")); + } + + ServicesDTos servicesDTos = new + ( + ServiceId: service.ServicesId, + Name: service.Name, + Price: service.Price, + Description: service.Description, + DurationInMinutes: service.DurationInMinutes, + ImageUrl: service.ImageUrl, + Status: service.Status, + RegisteredCompanyId: service.RegisteredCompanyId, + CreatedAt: service.CreatedAt, + UpdateAt: service.UpdateAt + ); + + _logger.LogInformation("Successfully retrieved {ServiceId} service.", service.ServicesId); + + return ResultT.Success(servicesDTos); + } + + public async Task> CreateAsync(CreateServiceDTos createAppointmentDTos, CancellationToken cancellationToken) + { + if (createAppointmentDTos == null) + { + _logger.LogError("Failed to create service. The request body is null."); + + return ResultT.Failure(Error.Failure("400", "Invalid request. Service data is required.")); + } + + string? logoUrl = null; + if (createAppointmentDTos.ImageFile != null) + { + using var stream = createAppointmentDTos.ImageFile.OpenReadStream(); + logoUrl = await _cloudinaryService.UploadImageCloudinaryAsync( + stream, + createAppointmentDTos.ImageFile.FileName, + cancellationToken); + } + + Domain.Models.Services service = new() + { + ServicesId = Guid.NewGuid(), + Name = createAppointmentDTos.Name, + Price = createAppointmentDTos.Price, + Description = createAppointmentDTos.Description, + DurationInMinutes = createAppointmentDTos.DurationInMinutes, + ImageUrl = logoUrl, + Status = Status.Activated, + RegisteredCompanyId = createAppointmentDTos.RegisteredCompanyId, + }; + + await _repository.AddAsync(service, cancellationToken); + + ServicesDTos servicesDTos = new + ( + ServiceId: service.ServicesId, + Name: service.Name, + Price: service.Price, + Description: service.Description, + DurationInMinutes: service.DurationInMinutes, + ImageUrl: service.ImageUrl, + Status: service.Status, + RegisteredCompanyId: service.RegisteredCompanyId, + CreatedAt: service.CreatedAt, + UpdateAt: service.UpdateAt + ); + _logger.LogInformation("Service created successfully with ID: {ServiceId}.", service.ServicesId); + + return ResultT.Success(servicesDTos); + } + + public async Task> UpdateAsync(Guid id, UpdateServiceDTos entity, CancellationToken cancellation) + { + var service = await _repository.GetByIdAsync(id, cancellation); + if (service == null) + { + _logger.LogError("Service update failed. Service with ID {ServiceId} not found.", id); + + return ResultT.Failure(Error.NotFound("404", $"{id} Service not found.")); + } + + service.Name = entity.Name; + service.Price = entity.Price; + service.Description = entity.Description; + service.DurationInMinutes = entity.DurationInMinutes; + service.UpdateAt = DateTime.UtcNow; + + await _repository.UpdateAsync(service, cancellation); + ServicesDTos servicesDTos = new + ( + ServiceId: service.ServicesId, + Name: service.Name, + Price: service.Price, + Description: service.Description, + DurationInMinutes: service.DurationInMinutes, + ImageUrl: service.ImageUrl, + Status: service.Status, + RegisteredCompanyId: service.RegisteredCompanyId, + CreatedAt: service.CreatedAt, + UpdateAt: service.UpdateAt + ); + + _logger.LogInformation("Service with ID {ServiceId} updated successfully.", id); + + return ResultT.Success(servicesDTos); + } + + public async Task> DeleteAsync(Guid id, CancellationToken cancellation) + { + var service = await _repository.GetByIdAsync(id, cancellation); + if (service == null) + { + _logger.LogError(""); + + return ResultT.Failure(Error.NotFound("404", $"{id} Service not found.")); + } + + await _repository.DeleteAsync(service, cancellation); + + _logger.LogInformation("Service with ID {ServiceId} deleted successfully.", id); + + return ResultT.Success(service.ServicesId ?? Guid.Empty); + } + + // string return + public async Task> ApplyDiscountCodeAsync(Guid serviceId, Guid registeredCompanyId, string discountCode, CancellationToken cancellationToken) + { + + var service = await _repository.GetByIdAsync(serviceId, cancellationToken); + if (service == null) + { + _logger.LogError("Service with ID {ServiceId} not found.", serviceId); + + return ResultT.Failure(Error.NotFound("404", $"{serviceId} Service not found.")); + } + + var company = await _registeredCompanyRepository.GetByIdAsync(registeredCompanyId, cancellationToken); + if (company == null) + { + _logger.LogError("Company with ID {CompanyId} not found.", registeredCompanyId); + + return ResultT.Failure(Error.NotFound("404", $"{registeredCompanyId} not found.")); + } + + if (string.IsNullOrWhiteSpace(discountCode)) + { + _logger.LogError("Invalid discount code provided. Discount code cannot be null or empty."); + + return ResultT.Failure(Error.Failure("400", $"The discount code {discountCode} is invalid.")); + } + + await _repository.ApplyDiscountCodeAsync(service, registeredCompanyId, discountCode, cancellationToken); + + _logger.LogInformation("Successfully applied discount code {DiscountCode} to service ID {ServiceId} for company ID {CompanyId}.", + discountCode, serviceId, registeredCompanyId); + + return ResultT.Success(service.ServicesId ?? Guid.Empty); + } + + public async Task>> GetServicesWithDurationLessThan30MinutesAsync(Guid registeredCompany, CancellationToken cancellationToken) + { + var company = await _registeredCompanyRepository.GetByIdAsync(registeredCompany, cancellationToken); + if (company == null) + { + _logger.LogError("Company with ID {CompanyId} not found.", registeredCompany); + + return ResultT>.Failure(Error.NotFound("404", $"Company with ID {registeredCompany} not found.")); + } + + var services = await _repository.GetServicesWithDurationLessThan30MinutesAsync(registeredCompany, cancellationToken); + if (!services.Any()) + { + _logger.LogWarning("No services found for company ID {CompanyId} with duration less than 30 minutes.", registeredCompany); + + return ResultT>.Failure(Error.Failure("400", "No services available with duration less than 30 minutes.")); + } + + IEnumerable serviceFilterDTos = services.Select(x => new ServiceFilterDTos + ( + Name: x.Name, + Price:x.Price, + Description: x.Description, + DurationInMinutes: x.DurationInMinutes, + ImageUrl: x.ImageUrl + )); + + _logger.LogInformation("Retrieved {ServiceCount} services with duration less than 30 minutes for company ID {CompanyId}.", + serviceFilterDTos.Count(), registeredCompany); + + return ResultT>.Success(serviceFilterDTos); + } + + public async Task>> GetServicesByCompanyIdAsync(Guid registeredCompaniesId, CancellationToken cancellationToken) + { + var company = await _registeredCompanyRepository.GetByIdAsync(registeredCompaniesId, cancellationToken); + if (company == null) + { + _logger.LogError("Company with ID {CompanyId} not found.", registeredCompaniesId); + + return ResultT>.Failure(Error.NotFound("404", $"Company with ID {registeredCompaniesId} not found.")); + } + + var servicesByRegisteredCompanies = + await _repository.GetServicesByCompanyIdAsync(registeredCompaniesId, cancellationToken); + + if (servicesByRegisteredCompanies == null || !servicesByRegisteredCompanies.Any()) + { + _logger.LogError("No services found for company ID {CompanyId}.", registeredCompaniesId); + + return ResultT>.Failure(Error.Failure("400", "No services found for company ID.")); + } + + IEnumerable servicesDTos = servicesByRegisteredCompanies.Select(x => new ServicesDTos + ( + ServiceId: x.ServicesId, + Name: x.Name, + Price: x.Price, + Description: x.Description, + DurationInMinutes: x.DurationInMinutes, + ImageUrl: x.ImageUrl, + Status: x.Status, + RegisteredCompanyId: x.RegisteredCompanyId, + CreatedAt: x.CreatedAt, + UpdateAt: x.UpdateAt + )); + + _logger.LogInformation("Retrieved {Count} services for company ID {CompanyId}.", servicesDTos.Count(), registeredCompaniesId); + + return ResultT>.Success(servicesDTos); + } + + public async Task>> GetServicesByNameAsync(string name, Guid registeredCompanyId, CancellationToken cancellationToken) + { + if (String.IsNullOrWhiteSpace(name)) + { + _logger.LogError("Service search failed. The provided service name is null or empty."); + + return ResultT>.Failure(Error.Failure("400", "Service name cannot be empty.")); + } + + var exists = await _repository.ValidateAsync(x => x.Name == name); + if (!exists) + { + _logger.LogError("Service search failed. No service found with the name '{ServiceName}'.", name); + + return ResultT>.Failure(Error.Failure("400", "No service found with the given name.")); + } + + var servicesByName = await _repository.GetServicesByNameAsync(registeredCompanyId,name, cancellationToken); + if (!servicesByName.Any()) + { + _logger.LogError("No services found for the company with ID {CompanyId} and service name '{ServiceName}'.", registeredCompanyId, name); + + return ResultT>.Failure(Error.Failure("400", "The service name is invalid.")); + } + + var servicesList = servicesByName.Select(x => new ServiceFilterDTos + ( + Name: x.Name, + Price: x.Price, + Description: x.Description, + DurationInMinutes: x.DurationInMinutes, + ImageUrl: x.ImageUrl + )); + + _logger.LogInformation("Successfully retrieved {ServiceCount} services for the company with ID {CompanyId} and service name '{ServiceName}'.", + servicesList.Count(), registeredCompanyId, name); + + return ResultT>.Success(servicesList); + } + + public async Task>> GetServicesByPriceAsync(decimal price, Guid registeredCompanyId, CancellationToken cancellationToken) + { + if (price <= 0) + { + _logger.LogError("Service search failed. The provided price '{Price}' is invalid.", price); + + return ResultT>.Failure(Error.Failure("400", "Price must be greater than zero.")); + } + + var serviceByPrice = await _repository.GetServicesByPriceAsync(registeredCompanyId, price, cancellationToken); + if (!serviceByPrice.Any()) + { + _logger.LogError("No services found for the company with ID {CompanyId} at price {Price}.", registeredCompanyId, price); + + return ResultT>.Failure(Error.Failure("400", "No services found at the specified price.")); + } + IEnumerable serviceFilterDTos = serviceByPrice.Select(x => new ServiceFilterDTos + ( + Name: x.Name, + Price: x.Price, + Description: x.Description, + DurationInMinutes: x.DurationInMinutes, + ImageUrl: x.ImageUrl + )); + _logger.LogInformation("Successfully retrieved {ServiceCount} services for the company with ID {CompanyId} at price {Price}.", + serviceFilterDTos.Count(), registeredCompanyId, price); + + return ResultT>.Success(serviceFilterDTos); + } +} \ No newline at end of file diff --git a/Trimly.Core.Application/Trimly.Core.Application.csproj b/Trimly.Core.Application/Trimly.Core.Application.csproj index 90abbae..a9ddd3b 100644 --- a/Trimly.Core.Application/Trimly.Core.Application.csproj +++ b/Trimly.Core.Application/Trimly.Core.Application.csproj @@ -8,6 +8,7 @@ + diff --git a/Trimly.Core.Domain/Models/Appointments.cs b/Trimly.Core.Domain/Models/Appointments.cs index 9993d95..a6c457c 100644 --- a/Trimly.Core.Domain/Models/Appointments.cs +++ b/Trimly.Core.Domain/Models/Appointments.cs @@ -11,7 +11,7 @@ public sealed class Appointments : CreationDateUpdate public DateTime? EndDateTime { get; set; } - public AppointmentStatus? AppointmentStatus { get; set; } + public AppointmentStatus AppointmentStatus { get; set; } public string? ConfirmationCode { get; set; } diff --git a/Trimly.Infrastructure.Persistence/AddPersistence.cs b/Trimly.Infrastructure.Persistence/AddPersistence.cs index 8b6e03b..91ac83a 100644 --- a/Trimly.Infrastructure.Persistence/AddPersistence.cs +++ b/Trimly.Infrastructure.Persistence/AddPersistence.cs @@ -7,7 +7,9 @@ using System.Runtime.CompilerServices; using System.Text; using System.Threading.Tasks; +using Trimly.Core.Application.Interfaces.Repository; using Trimly.Infrastructure.Persistence.Context; +using Trimly.Infrastructure.Persistence.Repository; namespace Trimly.Infrastructure.Persistence { @@ -24,6 +26,15 @@ public static void AddPersistenceMethod(this IServiceCollection Services, IConfi }); #endregion + + #region DI + Services.AddTransient (); + Services.AddTransient(); + Services.AddTransient(); + Services.AddTransient(); + Services.AddTransient(); + Services.AddTransient(typeof(IGenericRepository<>), typeof(GenericRepository<>)); + #endregion } } diff --git a/Trimly.Infrastructure.Persistence/Repository/AppointmentRepository.cs b/Trimly.Infrastructure.Persistence/Repository/AppointmentRepository.cs index 3ec0a6c..f4d4a94 100644 --- a/Trimly.Infrastructure.Persistence/Repository/AppointmentRepository.cs +++ b/Trimly.Infrastructure.Persistence/Repository/AppointmentRepository.cs @@ -10,8 +10,16 @@ public class AppointmentRepository : GenericRepository, IAppointme { public AppointmentRepository(TrimlyContext context) : base(context){} + public async Task ValidateAppointmentAsync(DateTime startDate, DateTime endDate,CancellationToken cancellationToken) + { + var exists = await _context.Set() + .AsNoTracking() + .Where(x => x.StartDateTime < endDate && x.EndDateTime > startDate) + .AnyAsync(cancellationToken); + + return exists; + } - public async Task CancelAppointmentAsync(Appointments appointments, CancellationToken cancellationToken) { appointments.AppointmentStatus = AppointmentStatus.Cancelled; @@ -48,6 +56,7 @@ await _context.Set() public async Task RescheduleAppointmentAsync(Appointments appointment, CancellationToken cancellationToken) { + appointment.AppointmentStatus = AppointmentStatus.Rescheduled; _context.Update(appointment); await SaveChangesAsync(cancellationToken); } diff --git a/Trimly.Infrastructure.Persistence/Repository/GenericRepository.cs b/Trimly.Infrastructure.Persistence/Repository/GenericRepository.cs index be098c5..1afa1de 100644 --- a/Trimly.Infrastructure.Persistence/Repository/GenericRepository.cs +++ b/Trimly.Infrastructure.Persistence/Repository/GenericRepository.cs @@ -1,4 +1,5 @@ -using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; using Trimly.Core.Application.Interfaces.Repository; using Trimly.Core.Application.Pagination; using Trimly.Infrastructure.Persistence.Context; @@ -41,7 +42,12 @@ public async Task> GetPagedResultAsync(int pageNumber, int pageSi } public async Task SaveChangesAsync(CancellationToken cancellationToken) => await _context.SaveChangesAsync(); - + public async Task ValidateAsync(Expression> predicate) + { + var exists = await _context.Set().AnyAsync(predicate); + return exists; + } + public async Task UpdateAsync(T entity, CancellationToken cancellationToken) { _context.Attach(entity); diff --git a/Trimly.Infrastructure.Persistence/Repository/SchedulesRepository.cs b/Trimly.Infrastructure.Persistence/Repository/SchedulesRepository.cs index 0456f41..b49def2 100644 --- a/Trimly.Infrastructure.Persistence/Repository/SchedulesRepository.cs +++ b/Trimly.Infrastructure.Persistence/Repository/SchedulesRepository.cs @@ -37,5 +37,11 @@ await _context.Set() .Include(s => s.RegisteredCompanies) .AsSplitQuery() .ToListAsync(cancellationToken); + + public async Task GetScheduleByCompanyIdAsync(Guid companyId, CancellationToken cancellationToken) => + await _context.Set() + .AsNoTracking() + .Where(s => s.RegisteredCompanyId == companyId) + .FirstOrDefaultAsync(cancellationToken); } } diff --git a/Trimly.Infrastructure.Persistence/Repository/ServiceRepository.cs b/Trimly.Infrastructure.Persistence/Repository/ServiceRepository.cs index bf732c7..802f22a 100644 --- a/Trimly.Infrastructure.Persistence/Repository/ServiceRepository.cs +++ b/Trimly.Infrastructure.Persistence/Repository/ServiceRepository.cs @@ -32,11 +32,18 @@ public async Task ApplyDiscountCodeAsync(Services services, Guid registeredCompa } + public async Task> GetServicesByCompanyIdAsync(Guid companyId, + CancellationToken cancellationToken) => + await _context.Set() + .AsNoTracking() + .Where(x => x.RegisteredCompanyId == companyId) + .ToListAsync(cancellationToken); + public async Task> GetServicesByDurationInMinutesAsync(Guid registeredCompaniesId, int durationInMinutes, CancellationToken cancellationToken) => await _context.Set() .AsNoTracking() .Where(d => d.DurationInMinutes == durationInMinutes) - .ToListAsync(); + .ToListAsync(cancellationToken); public async Task> GetServicesByNameAsync(Guid registeredCompaniesId, string name, CancellationToken cancellationToken) => await _context.Set()