diff --git a/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs b/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs new file mode 100644 index 00000000..6a7398cc --- /dev/null +++ b/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs @@ -0,0 +1,57 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using ShoeStore.Application.DTOs; +using ShoeStore.Application.DTOs.VoucherDtos; +using ShoeStore.Application.Interface.VoucherInterface; + +namespace ShoeStore.Api.Controllers; + +/// +/// Controller for managing user-specific voucher operations. +/// Provides endpoints for users to retrieve their own vouchers. +/// +/// Service for handling user-voucher relationship operations. +[ApiController] +[Route("api/user/vouchers")] +[Authorize(Roles = "User")] +public class UserVoucherController(IUserVoucherService userVoucherService) : ControllerBase +{ + /// + /// Retrieves all active vouchers associated with a specific user. + /// + /// + /// Requires User role authorization. + /// Returns a list of vouchers that are currently valid and assigned to the user. + /// + /// The unique identifier (GUID) of the user whose vouchers are being retrieved. + /// Cancellation token for the request. + /// Vouchers retrieved successfully. + /// Unauthorized; user must be authenticated with User role. + /// Not found; no vouchers found for the specified user. + /// Internal server error; an unexpected error occurred. + /// An action result containing a paginated list of user vouchers on success, or an error response. + [ProducesResponseType(typeof(PageResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status401Unauthorized)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + [HttpGet("user/{userGuid}")] + public async Task GetVouchersForUser(Guid userGuid, CancellationToken token) + { + var result = await userVoucherService.GetAllVoucherForUserAsync(userGuid, token); + return result.Match( + vouchers => Ok(vouchers), + errors => errors[0].Code switch + { + "NO_VOUCHERS_FOUND" => NotFound(new + { + message = "No vouchers found for this user", + detail = errors[0].Description + }), + _ => BadRequest(new + { + message = "Failed to retrieve vouchers for user", + detail = errors[0].Description + }) + }); + } +} diff --git a/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs b/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs new file mode 100644 index 00000000..54d840fb --- /dev/null +++ b/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs @@ -0,0 +1,243 @@ +using ErrorOr; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using ShoeStore.Application.DTOs; +using ShoeStore.Application.DTOs.VoucherDtos; +using ShoeStore.Application.Interface.VoucherInterface; + +namespace ShoeStore.Api.Controllers; + +/// +/// Controller for managing vouchers in the system. +/// Provides endpoints for voucher creation, retrieval, update, and deletion (Admin only). +/// +/// Service for handling voucher logic operations. +[ApiController] +[Route("api/admin/vouchers")] +[Authorize(Roles = "Admin")] +public class VoucherController(IVoucherService voucherService) : ControllerBase +{ + /// + /// Creates a new voucher for the store. + /// + /// + /// Requires Admin role authorization. + /// The request body should include: + /// - VoucherName: Name of the voucher + /// - Discount: Value of the discount + /// - DiscountType: Type of discount (Percentage/FixedAmount) + /// - TotalQuantity: Number of vouchers available + /// - ValidFrom/ValidTo: Expiration dates + /// + /// Data transfer object containing voucher creation details. + /// Cancellation token for the request. + /// Voucher created successfully. + /// Bad request; invalid voucher data provided. + /// Unauthorized; user must be authenticated with Admin role. + /// Internal server error; an unexpected error occurred. + /// An action result with status 201 (Created) on success, or an error response. + [ProducesResponseType(typeof(object), StatusCodes.Status201Created)] + [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(object), StatusCodes.Status401Unauthorized)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + [HttpPost] + public async Task CreateVoucher([FromBody] CreateVoucherDto createVoucherDto, CancellationToken token) + { + var result = await voucherService.CreateVoucherAsync(createVoucherDto, token); + + return result.Match( + _ => Created("", new { message = "Voucher created successfully" }), + errors => BadRequest(new + { + message = "Failed to create voucher", + detail = errors[0].Description + })); + } + + /// + /// Updates an existing voucher's details. + /// + /// + /// Requires Admin role authorization. + /// Updates the specified voucher with the provided information. + /// + /// The unique identifier (GUID) of the voucher to update. + /// Data transfer object containing updated voucher details. + /// Cancellation token for the request. + /// Voucher updated successfully. + /// Bad request; invalid update data provided. + /// Unauthorized; user must be authenticated with Admin role. + /// Not found; the voucher with the specified ID does not exist. + /// Internal server error; an unexpected error occurred. + /// An action result with status 200 (OK) on success, or an error response. + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(object), StatusCodes.Status401Unauthorized)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + [HttpPut("{voucherGuid}")] + public async Task UpdateVoucher(Guid voucherGuid, [FromBody] UpdateVoucherDto updateVoucherDto, + CancellationToken token) + { + var result = await voucherService.UpdateVoucherAsync(voucherGuid, updateVoucherDto, token); + return result.Match( + _ => Ok(new { message = "Voucher updated successfully" }), + errors => errors[0].Code switch + { + "VOUCHER_NOT_FOUND" => NotFound(new + { + message = "Voucher not found", + detail = errors[0].Description + }), + _ => BadRequest(new + { + message = "Failed to update voucher", + detail = errors[0].Description + }) + }); + } + + /// + /// Deletes a specific voucher from the system (soft delete). + /// + /// + /// Requires Admin role authorization. + /// Performs a soft delete by marking the voucher as deleted. + /// + /// The unique identifier (GUID) of the voucher to delete. + /// Cancellation token for the request. + /// Voucher deleted successfully. + /// Unauthorized; user must be authenticated with Admin role. + /// Not found; the voucher with the specified ID does not exist. + /// Internal server error; an unexpected error occurred. + /// An action result with status 200 (OK) on success, or an error response. + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status401Unauthorized)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + [HttpDelete("{voucherGuid}")] + public async Task DeleteVoucher(Guid voucherGuid, CancellationToken token) + { + var result = await voucherService.DeleteVoucherByGuidAsync(voucherGuid, token); + return result.Match( + _ => Ok(new { message = "Voucher deleted successfully" }), + errors => errors[0].Code switch + { + "VOUCHER_NOT_FOUND" => NotFound(new + { + message = "Voucher not found", + detail = errors[0].Description + }), + _ => BadRequest(new + { + message = "Failed to delete voucher", + detail = errors[0].Description + }) + }); + } + + /// + /// Deletes all expired vouchers from the system (soft delete). + /// + /// + /// Requires Admin role authorization. + /// Identifies and soft deletes all vouchers whose expiration date has passed. + /// + /// Cancellation token for the request. + /// Expired vouchers deleted successfully. + /// Bad request; failed to delete expired vouchers or no expired vouchers found. + /// Unauthorized; user must be authenticated with Admin role. + /// Internal server error; an unexpected error occurred. + /// An action result with status 200 (OK) on success, or an error response. + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)] + [ProducesResponseType(typeof(object), StatusCodes.Status401Unauthorized)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + [HttpDelete("expire")] + public async Task DeleteExpiredVouchers(CancellationToken token) + { + var result = await voucherService.DeleteVoucherExpireAsync(token); + return result.Match( + _ => Ok(new { message = "Expired vouchers deleted successfully" }), + errors => BadRequest(new + { + message = "Failed to delete expired vouchers", + detail = errors[0].Description + })); + } + + /// + /// Retrieves a paginated list of vouchers for administrative purposes. + /// + /// + /// Requires Admin role authorization. + /// Provides a list of vouchers with detailed information relevant for administrators. + /// + /// Cancellation token for the request. + /// Vouchers retrieved successfully. + /// Unauthorized; user must be authenticated with Admin role. + /// Not found; no vouchers found in the system. + /// Internal server error; an unexpected error occurred. + /// An action result containing a paginated list of vouchers on success, or an error response. + [ProducesResponseType(typeof(PageResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status401Unauthorized)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + [HttpGet] + public async Task GetVouchersForAdmin(CancellationToken token) + { + var result = await voucherService.GetVoucherForAdminAsync(token); + return result.Match( + vouchers => Ok(vouchers), + errors => errors[0].Code switch + { + "NO_VOUCHERS_FOUND" => NotFound(new + { + message = "No vouchers found", + detail = errors[0].Description + }), + _ => BadRequest(new + { + message = "Failed to retrieve vouchers", + detail = errors[0].Description + }) + }); + } + + /// + /// Retrieves all active and non-deleted vouchers in the system. + /// + /// + /// Requires Admin role authorization. + /// + /// Cancellation token for the request. + /// All vouchers retrieved successfully. + /// Unauthorized; user must be authenticated with Admin role. + /// Not found; no vouchers found in the system. + /// Internal server error; an unexpected error occurred. + /// An action result containing a list of vouchers on success, or an error response. + [ProducesResponseType(typeof(PageResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(object), StatusCodes.Status401Unauthorized)] + [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] + [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] + [HttpGet("all")] + public async Task GetAllVouchers(CancellationToken token) + { + var result = await voucherService.GetAllVouchersAsync(token); + return result.Match( + vouchers => Ok(vouchers), + errors => errors[0].Code switch + { + "NO_VOUCHERS_FOUND" => NotFound(new + { + message = "No vouchers found", + detail = errors[0].Description + }), + _ => BadRequest(new + { + message = "Failed to retrieve all vouchers", + detail = errors[0].Description + }) + }); + } +} diff --git a/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/CreateVoucherDto.cs b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/CreateVoucherDto.cs new file mode 100644 index 00000000..d18c7d7b --- /dev/null +++ b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/CreateVoucherDto.cs @@ -0,0 +1,29 @@ + +namespace ShoeStore.Application.DTOs.VoucherDtos +{ + public class CreateVoucherDto + { + public string? VoucherName { get; set; } + public string? VoucherDescription { get; set; } + public decimal? Discount { get; set; } + public int VoucherScope { get; set; } = 1; // Default to AllProducts + public int DiscountType { get; set; } = 1; // Default to Percentage + public decimal MaxPriceDiscount { get; set; } + private DateTime? _validFrom; + public DateTime? ValidFrom + { + get => _validFrom; + set => _validFrom = value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null; + } + + private DateTime? _validTo; + public DateTime? ValidTo + { + get => _validTo; + set => _validTo = value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null; + } + public int? MaxUsagePerUser { get; set; } + public int? TotalQuantity { get; set; } + public decimal? MinOrderPrice { get; set; } + } +} diff --git a/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/DeleteVoucherDto.cs b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/DeleteVoucherDto.cs new file mode 100644 index 00000000..5a1f142e --- /dev/null +++ b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/DeleteVoucherDto.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ShoeStore.Application.DTOs.VoucherDtos +{ + public class DeleteVoucherDto + { + public bool IsDeleted { get; set; } = true; + } +} diff --git a/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherAdminDto.cs b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherAdminDto.cs new file mode 100644 index 00000000..04b00391 --- /dev/null +++ b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherAdminDto.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ShoeStore.Application.DTOs.VoucherDtos +{ + public class ResponseVoucherAdminDto + { + public string? VoucherName { get; set; } + public decimal Discount { get; set; } = 0; + public int? VoucherScope { get; set; } + public int? DiscountType { get; set; } + public decimal? MaxPriceDiscount { get; set; } + public decimal? MinOrderPrice { get; set; } + private DateTime? _validFrom; + public DateTime? ValidFrom + { + get => _validFrom; + set => _validFrom = value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null; + } + + private DateTime? _validTo; + public DateTime? ValidTo + { + get => _validTo; + set => _validTo = value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null; + } + } +} diff --git a/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherUserDto.cs b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherUserDto.cs new file mode 100644 index 00000000..5e6bbf4a --- /dev/null +++ b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherUserDto.cs @@ -0,0 +1,23 @@ + +namespace ShoeStore.Application.DTOs.VoucherDtos +{ + public class ResponseVoucherUserDto + { + public string VoucherName { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public decimal Discount { get; set; } = 0; + private DateTime? _validFrom; + public DateTime? ValidFrom + { + get => _validFrom; + set => _validFrom = value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null; + } + + private DateTime? _validTo; + public DateTime? ValidTo + { + get => _validTo; + set => _validTo = value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null; + } + } +} diff --git a/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/UpdateVoucherDto.cs b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/UpdateVoucherDto.cs new file mode 100644 index 00000000..1da41e96 --- /dev/null +++ b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/UpdateVoucherDto.cs @@ -0,0 +1,27 @@ +namespace ShoeStore.Application.DTOs.VoucherDtos +{ + public class UpdateVoucherDto + { + public string? VoucherDescription { get; set; } + public decimal? Discount { get; set; } + public int VoucherScope { get; set; } = 1; // Default to AllProducts + public int DiscountType { get; set; } = 1; // Default to Percentage + public decimal MaxPriceDiscount { get; set; } + private DateTime? _validFrom; + public DateTime? ValidFrom + { + get => _validFrom; + set => _validFrom = value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null; + } + + private DateTime? _validTo; + public DateTime? ValidTo + { + get => _validTo; + set => _validTo = value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null; + } + public int? MaxUsagePerUser { get; set; } + public int? TotalQuantity { get; set; } + public decimal? MinOrderPrice { get; set; } + } +} diff --git a/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IUserVoucherRepository.cs b/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IUserVoucherRepository.cs new file mode 100644 index 00000000..b32bc5be --- /dev/null +++ b/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IUserVoucherRepository.cs @@ -0,0 +1,10 @@ +using ShoeStore.Domain.Entities; + +namespace ShoeStore.Application.Interface.VoucherInterface +{ + public interface IUserVoucherRepository + { + IQueryable GetAllVouchers(); + IQueryable GetVouchersByUserGuid(Guid userGuid); + } +} diff --git a/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IUserVoucherService.cs b/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IUserVoucherService.cs new file mode 100644 index 00000000..fbc58797 --- /dev/null +++ b/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IUserVoucherService.cs @@ -0,0 +1,10 @@ +using ShoeStore.Application.DTOs.VoucherDtos; +using ShoeStore.Application.DTOs; +using ErrorOr; +namespace ShoeStore.Application.Interface.VoucherInterface +{ + public interface IUserVoucherService : IVoucherService + { + Task>> GetAllVoucherForUserAsync(Guid UserGuid, CancellationToken token); + } +} diff --git a/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherRepository.cs b/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherRepository.cs new file mode 100644 index 00000000..117cf165 --- /dev/null +++ b/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherRepository.cs @@ -0,0 +1,11 @@ +using ShoeStore.Application.Interface.Common; +using ShoeStore.Domain.Entities; + +namespace ShoeStore.Application.Interface.VoucherInterface +{ + public interface IVoucherRepository : IGenericRepository + { + IQueryable GetAllVouchers(); + IQueryable GetVoucherByGuid(Guid voucherGuid); + } +} diff --git a/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs b/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs new file mode 100644 index 00000000..d364bbab --- /dev/null +++ b/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs @@ -0,0 +1,19 @@ +using ShoeStore.Application.DTOs.VoucherDtos; +using ErrorOr; +using ShoeStore.Application.DTOs; +namespace ShoeStore.Application.Interface.VoucherInterface +{ + public interface IVoucherService + { + // CREATE + Task> CreateVoucherAsync(CreateVoucherDto voucherCreateDto, CancellationToken token); + // GET + Task>> GetVoucherForAdminAsync(CancellationToken token); + Task>> GetAllVouchersAsync(CancellationToken token); + // UPDATE + Task> UpdateVoucherAsync(Guid voucherGuid, UpdateVoucherDto voucherUpdateDto, CancellationToken token); + // DELETE + Task> DeleteVoucherByGuidAsync(Guid voucherGuid, CancellationToken token ); + Task> DeleteVoucherExpireAsync(CancellationToken token); + } +} diff --git a/Backend/src/ShoeStore.Application/Services/VoucherService.cs b/Backend/src/ShoeStore.Application/Services/VoucherService.cs new file mode 100644 index 00000000..903fdc80 --- /dev/null +++ b/Backend/src/ShoeStore.Application/Services/VoucherService.cs @@ -0,0 +1,230 @@ +using ShoeStore.Application.Interface.VoucherInterface; +using ShoeStore.Application.DTOs.VoucherDtos; +using ErrorOr; +using ShoeStore.Application.Interface.Common; +using ShoeStore.Domain.Entities; +using ShoeStore.Domain.Enum; +using Microsoft.EntityFrameworkCore; +using ShoeStore.Application.DTOs; + +namespace ShoeStore.Application.Services +{ + public class VoucherService : IVoucherService, IUserVoucherService + { + private readonly IVoucherRepository voucherRepository; + private readonly IUnitOfWork uow; + private readonly IUserVoucherRepository userVoucherRepository; + + public VoucherService(IVoucherRepository voucherRepository, IUnitOfWork uow, IUserVoucherRepository userVoucherRepository) + { + this.voucherRepository = voucherRepository; + this.uow = uow; + this.userVoucherRepository = userVoucherRepository; + } + public async Task> CreateVoucherAsync(CreateVoucherDto voucherCreateDto, CancellationToken token) + { + + var voucher = new Voucher + { + VoucherName = voucherCreateDto.VoucherName ?? string.Empty, + VoucherDescription = voucherCreateDto.VoucherDescription, + Discount = voucherCreateDto.Discount ?? 0, + VoucherScope = (VoucherScope)voucherCreateDto.VoucherScope, + DiscountType = (DiscountType)voucherCreateDto.DiscountType, + MaxPriceDiscount = voucherCreateDto.MaxPriceDiscount, + ValidFrom = voucherCreateDto.ValidFrom, + ValidTo = voucherCreateDto.ValidTo, + MaxUsagePerUser = voucherCreateDto.MaxUsagePerUser, + TotalQuantity = voucherCreateDto.TotalQuantity ?? 0, + MinOrderPrice = voucherCreateDto.MinOrderPrice ?? 0, + IsDeleted = false + }; + + voucherRepository.Add(voucher); + await uow.SaveChangesAsync(token); + return Result.Created; + + } + + public async Task> DeleteVoucherByGuidAsync(Guid voucherGuid, CancellationToken token) + { + var voucher = await voucherRepository + .GetVoucherByGuid(voucherGuid) + .Where(v => !v.IsDeleted) + .FirstOrDefaultAsync(); + if (voucher == null) + { + return Error.NotFound( + "VOUCHER_NOT_FOUND", + "The voucher with the specified GUID does not exist." + ); + } + // Soft delete logic + voucher.IsDeleted = true; + voucher.UpdatedAt = DateTime.UtcNow; + voucherRepository.Update(voucher); + await uow.SaveChangesAsync(token); + return Result.Deleted; + } + + public async Task> DeleteVoucherExpireAsync(CancellationToken token) + { + var vouchersToDelete = voucherRepository + .GetAllVouchers() + .Where(v => v.ValidTo < DateTime.UtcNow && !v.IsDeleted) + .ToList(); + if(!vouchersToDelete.Any()) + { + return Error.NotFound( + "NO_EXPIRED_VOUCHERS", + "There are no expired vouchers to delete." + ); + } + foreach (var voucher in vouchersToDelete) + { + voucher.IsDeleted = true; + voucher.UpdatedAt = DateTime.UtcNow; + voucherRepository.Update(voucher); + } + await uow.SaveChangesAsync(token); + return Result.Deleted; + } + + public async Task>> GetAllVoucherForUserAsync(Guid UserGuid, CancellationToken token) + { + var vouchers = await userVoucherRepository + .GetVouchersByUserGuid(UserGuid) + .Where(v => !v.Voucher.IsDeleted && v.Voucher.ValidTo > DateTime.UtcNow) + .Select(v => new ResponseVoucherUserDto + { + VoucherName = v.Voucher.VoucherName ?? string.Empty, + Description = v.Voucher.VoucherDescription ?? string.Empty, + Discount = v.Voucher.Discount, + ValidFrom = v.Voucher.ValidFrom, + ValidTo = v.Voucher.ValidTo + }) + .ToListAsync(token); + + if(vouchers == null || !vouchers.Any()) + { + return Error.NotFound( + "NO_VOUCHERS_FOUND", + "No vouchers were found for the user." + ); + } + + var result = new PageResult + { + Items = vouchers, + TotalCount = vouchers.Count + }; + return result; + } + + public async Task>> GetAllVouchersAsync(CancellationToken token) + { + var vouchers = await voucherRepository + .GetAllVouchers() + .Where(v => !v.IsDeleted) + .Select(v => new ResponseVoucherAdminDto + { + VoucherName = v.VoucherName, + Discount = v.Discount, + VoucherScope = (int)v.VoucherScope, + DiscountType = (int)v.DiscountType, + MaxPriceDiscount = v.MaxPriceDiscount, + ValidFrom = v.ValidFrom, + ValidTo = v.ValidTo, + MinOrderPrice = v.MinOrderPrice + }) + .ToListAsync(token); + if(vouchers == null || !vouchers.Any()) + { + return Error.NotFound( + "NO_VOUCHERS_FOUND", + "Dont have voucher created" + ); + } + + var pageResult = new PageResult + { + Items = vouchers, + TotalCount = vouchers.Count + }; + return pageResult; + + } + + public async Task>> GetVoucherForAdminAsync(CancellationToken token) + { + var vouchers = await voucherRepository + .GetAllVouchers() + .Where(v => !v.IsDeleted) + .Select(v => new ResponseVoucherAdminDto + { + VoucherName = v.VoucherName, + Discount = v.Discount, + VoucherScope = (int)v.VoucherScope, + DiscountType = (int)v.DiscountType, + MaxPriceDiscount = v.MaxPriceDiscount, + ValidFrom = v.ValidFrom, + ValidTo = v.ValidTo, + MinOrderPrice = v.MinOrderPrice + }) + .ToListAsync(); + if(vouchers == null || !vouchers.Any()) + { + return Error.NotFound( + "NO_VOUCHERS_FOUND", + "No vouchers were found in the system." + ); + } + + var pageResult = new PageResult + { + Items = vouchers, + TotalCount = vouchers.Count + }; + return pageResult; + } + + public async Task> UpdateVoucherAsync(Guid voucherGuid, UpdateVoucherDto voucherUpdateDto, CancellationToken token) + { + var voucher = await voucherRepository + .GetVoucherByGuid(voucherGuid) + .FirstOrDefaultAsync(); + + if (voucher == null) + { + return Error.NotFound( + "VOUCHER_NOT_FOUND", + "The voucher with the specified GUID does not exist." + ); + } + + // Update logic + voucher.VoucherDescription = voucherUpdateDto.VoucherDescription ?? voucher.VoucherDescription; + + voucher.VoucherScope = (VoucherScope)voucherUpdateDto.VoucherScope; + voucher.DiscountType = (DiscountType)voucherUpdateDto.DiscountType; + + voucher.MaxPriceDiscount = voucherUpdateDto.MaxPriceDiscount; + + voucher.ValidFrom = voucherUpdateDto.ValidFrom ?? voucher.ValidFrom; + voucher.ValidTo = voucherUpdateDto.ValidTo ?? voucher.ValidTo; + + voucher.MaxUsagePerUser = voucherUpdateDto.MaxUsagePerUser ?? voucher.MaxUsagePerUser; + voucher.TotalQuantity = voucherUpdateDto.TotalQuantity ?? voucher.TotalQuantity; + voucher.MinOrderPrice = voucherUpdateDto.MinOrderPrice ?? voucher.MinOrderPrice; + + voucher.UpdatedAt = DateTime.UtcNow; + + voucherRepository.Update(voucher); + await uow.SaveChangesAsync(token); + + return Result.Updated; + + } + + } +} diff --git a/Backend/src/ShoeStore.Application/Validations/VoucherValidation/CreateVoucherDtoValidation.cs b/Backend/src/ShoeStore.Application/Validations/VoucherValidation/CreateVoucherDtoValidation.cs new file mode 100644 index 00000000..63e52f2d --- /dev/null +++ b/Backend/src/ShoeStore.Application/Validations/VoucherValidation/CreateVoucherDtoValidation.cs @@ -0,0 +1,26 @@ +using FluentValidation; +using ShoeStore.Application.DTOs.VoucherDtos; + +namespace ShoeStore.Application.Validations.VoucherValidation; + +public class CreateVoucherDtoValidation : AbstractValidator +{ + public CreateVoucherDtoValidation() + { + RuleFor(x => x.VoucherName) + .NotEmpty().WithMessage("Voucher name is required") + .MaximumLength(100).WithMessage("Voucher name must not exceed 100 characters"); + + RuleFor(x => x.Discount) + .NotNull().WithMessage("Discount is required") + .GreaterThanOrEqualTo(0).WithMessage("Discount must be greater than or equal to 0"); + + RuleFor(x => x.TotalQuantity) + .GreaterThan(0).WithMessage("Total quantity must be greater than 0"); + + RuleFor(x => x.ValidFrom) + .LessThanOrEqualTo(x => x.ValidTo) + .When(x => x.ValidFrom.HasValue && x.ValidTo.HasValue) + .WithMessage("Valid from date must be less than or equal to valid to date"); + } +} diff --git a/Backend/src/ShoeStore.Infrastructure/DependencyInjection/DependencyInjection.cs b/Backend/src/ShoeStore.Infrastructure/DependencyInjection/DependencyInjection.cs index 7e142081..898da647 100644 --- a/Backend/src/ShoeStore.Infrastructure/DependencyInjection/DependencyInjection.cs +++ b/Backend/src/ShoeStore.Infrastructure/DependencyInjection/DependencyInjection.cs @@ -20,6 +20,7 @@ using ShoeStore.Infrastructure.Notification; using ShoeStore.Infrastructure.Repositories; using ShoeStore.Infrastructure.RestorePassService; +using ShoeStore.Application.Interface.VoucherInterface; namespace ShoeStore.Infrastructure.DependencyInjection; @@ -62,6 +63,10 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); return services; } } \ No newline at end of file diff --git a/Backend/src/ShoeStore.Infrastructure/Repositories/UserVoucherRepository.cs b/Backend/src/ShoeStore.Infrastructure/Repositories/UserVoucherRepository.cs new file mode 100644 index 00000000..e8c83c4b --- /dev/null +++ b/Backend/src/ShoeStore.Infrastructure/Repositories/UserVoucherRepository.cs @@ -0,0 +1,20 @@ +using ShoeStore.Application.Interface.VoucherInterface; +using ShoeStore.Domain.Entities; +using ShoeStore.Infrastructure.Data; + +namespace ShoeStore.Infrastructure.Repositories +{ + public class UserVoucherRepository(AppDbContext context) : IUserVoucherRepository + { + public IQueryable GetAllVouchers() + { + return context.UserVouchers; + } + + public IQueryable GetVouchersByUserGuid(Guid userGuid) + { + return context.UserVouchers + .Where(uv => uv.User != null && uv.User.PublicId == userGuid); + } + } +} diff --git a/Backend/src/ShoeStore.Infrastructure/Repositories/VoucherRepository.cs b/Backend/src/ShoeStore.Infrastructure/Repositories/VoucherRepository.cs new file mode 100644 index 00000000..ee912222 --- /dev/null +++ b/Backend/src/ShoeStore.Infrastructure/Repositories/VoucherRepository.cs @@ -0,0 +1,20 @@ +using Microsoft.EntityFrameworkCore; +using ShoeStore.Application.Interface.VoucherInterface; +using ShoeStore.Domain.Entities; +using ShoeStore.Infrastructure.Data; + +namespace ShoeStore.Infrastructure.Repositories +{ + public class VoucherRepository(AppDbContext context) : GenericRepository(context), IVoucherRepository + { + public IQueryable GetAllVouchers() + { + return context.Vouchers.AsNoTracking(); + } + + public IQueryable GetVoucherByGuid(Guid voucherGuid) + { + return context.Vouchers.Where(v => v.PublicId == voucherGuid).AsNoTracking(); + } + } +}