From 36f76fefca3a0db3947d5dfe2b5e26ae03f6cdb2 Mon Sep 17 00:00:00 2001 From: Le Huu Viet Hoang Date: Sat, 18 Apr 2026 23:37:58 +0700 Subject: [PATCH 01/13] fix --- .../DTOs/VoucherDtos/CreateVoucherDto.cs | 18 ++++++++++++++++++ .../DTOs/VoucherDtos/UpdateVoucherDto.cs | 17 +++++++++++++++++ .../ShoeStore.Application.csproj | 18 +++++++++--------- 3 files changed, 44 insertions(+), 9 deletions(-) create mode 100644 Backend/src/ShoeStore.Application/DTOs/VoucherDtos/CreateVoucherDto.cs create mode 100644 Backend/src/ShoeStore.Application/DTOs/VoucherDtos/UpdateVoucherDto.cs 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..58172d61 --- /dev/null +++ b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/CreateVoucherDto.cs @@ -0,0 +1,18 @@ + +namespace ShoeStore.Application.DTOs.VoucherDtos +{ + public class CreateVoucherDto + { + public string? VocherName { get; set; } + public string? VoucherDescription { get; set; } + public decimal Discount { get; set; } + public int VoucherScope { get; set; } + public int DiscountType { get; set; } + public decimal MaxPriceDiscount { get; set; } + public DateTime? ValidFrom { get; set; } + public DateTime? ValidTo { get; set; } + public int? MaxUsagePerUser { get; set; } + public int TotalQuantity { get; set; } + public decimal MinOrderPrice { get; set; } + } +} 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..8aa0f591 --- /dev/null +++ b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/UpdateVoucherDto.cs @@ -0,0 +1,17 @@ +namespace ShoeStore.Application.DTOs.VoucherDtos +{ + public class UpdateVoucherDto + { + public string? VoucherDescription { get; set; } + public int? VoucherScope { get; set; } + public int? DiscountType { get; set; } + public decimal MaxPriceDiscount { get; set; } + public DateTime UpdateAt { get; set; } = DateTime.UtcNow; + public DateTime? ValidFrom { get; set; } + public DateTime? ValidTo { get; set; } + public int? MaxUsagePerUser { get; set; } + public int? TotalQuantity { get; set; } + public bool? IsDeleted { get; set; } = false; + public decimal? MinOrderPrice { get; set; }; + } +} diff --git a/Backend/src/ShoeStore.Application/ShoeStore.Application.csproj b/Backend/src/ShoeStore.Application/ShoeStore.Application.csproj index b3fc5e95..5eeec83e 100644 --- a/Backend/src/ShoeStore.Application/ShoeStore.Application.csproj +++ b/Backend/src/ShoeStore.Application/ShoeStore.Application.csproj @@ -12,15 +12,15 @@ - - - - - - - - - + + + + + + + + + From 3d26f358818ddd409c55aaa7e7bc93c1a2b13a37 Mon Sep 17 00:00:00 2001 From: Le Huu Viet Hoang Date: Sun, 19 Apr 2026 15:35:34 +0700 Subject: [PATCH 02/13] feat: adding API POST for Admin --- .../Controllers/VoucherController.cs | 55 ++++++++++++++++ .../DTOs/VoucherDtos/CreateVoucherDto.cs | 27 +++++--- .../DTOs/VoucherDtos/ResponseVoucherDto.cs | 11 ++++ .../DTOs/VoucherDtos/UpdateVoucherDto.cs | 2 +- .../VoucherInterface/IVoucherRepository.cs | 11 ++++ .../VoucherInterface/IVoucherService.cs | 13 ++++ .../Services/VoucherService.cs | 64 +++++++++++++++++++ .../CreateVoucherDtoValidation.cs | 26 ++++++++ .../DependencyInjection.cs | 3 + .../Repositories/VoucherRepository.cs | 19 ++++++ 10 files changed, 222 insertions(+), 9 deletions(-) create mode 100644 Backend/src/ShoeStore.Api/Controllers/VoucherController.cs create mode 100644 Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherDto.cs create mode 100644 Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherRepository.cs create mode 100644 Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs create mode 100644 Backend/src/ShoeStore.Application/Services/VoucherService.cs create mode 100644 Backend/src/ShoeStore.Application/Validations/VoucherValidation/CreateVoucherDtoValidation.cs create mode 100644 Backend/src/ShoeStore.Infrastructure/Repositories/VoucherRepository.cs diff --git a/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs b/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs new file mode 100644 index 00000000..f529299e --- /dev/null +++ b/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs @@ -0,0 +1,55 @@ +using ErrorOr; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +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 and management (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(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", + details = errors + })); + } +} diff --git a/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/CreateVoucherDto.cs b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/CreateVoucherDto.cs index 58172d61..d18c7d7b 100644 --- a/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/CreateVoucherDto.cs +++ b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/CreateVoucherDto.cs @@ -3,16 +3,27 @@ namespace ShoeStore.Application.DTOs.VoucherDtos { public class CreateVoucherDto { - public string? VocherName { get; set; } + public string? VoucherName { get; set; } public string? VoucherDescription { get; set; } - public decimal Discount { get; set; } - public int VoucherScope { get; set; } - public int DiscountType { 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; } - public DateTime? ValidFrom { get; set; } - public DateTime? ValidTo { 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; } + public int? TotalQuantity { get; set; } + public decimal? MinOrderPrice { get; set; } } } diff --git a/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherDto.cs b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherDto.cs new file mode 100644 index 00000000..2500cf67 --- /dev/null +++ b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherDto.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ShoeStore.Application.DTOs.VoucherDtos +{ + public class ResponseVoucherDto + { + + } +} diff --git a/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/UpdateVoucherDto.cs b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/UpdateVoucherDto.cs index 8aa0f591..8390842b 100644 --- a/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/UpdateVoucherDto.cs +++ b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/UpdateVoucherDto.cs @@ -12,6 +12,6 @@ public class UpdateVoucherDto public int? MaxUsagePerUser { get; set; } public int? TotalQuantity { get; set; } public bool? IsDeleted { get; set; } = false; - public decimal? MinOrderPrice { get; set; }; + public decimal? MinOrderPrice { get; set; } } } 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..105ccd99 --- /dev/null +++ b/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs @@ -0,0 +1,13 @@ +using ShoeStore.Application.DTOs.VoucherDtos; +using ErrorOr; +namespace ShoeStore.Application.Interface.VoucherInterface +{ + public interface IVoucherService + { + Task> CreateVoucherAsync(CreateVoucherDto voucherCreateDto, CancellationToken token); + //Task> GetVoucherByGuidAsync(Guid voucherGuid, CancellationToken token); + //Task>> GetAllVouchersAsync(CancellationToken token); + //Task> UpdateVoucherAsync(Guid voucherGuid, UpdateVoucherDto voucherUpdateDto, CancellationToken token); + //Task> DeleteVoucherAsync(Guid voucherGuid, 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..0ebf6136 --- /dev/null +++ b/Backend/src/ShoeStore.Application/Services/VoucherService.cs @@ -0,0 +1,64 @@ +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; +namespace ShoeStore.Application.Services +{ + public class VoucherService : IVoucherService + { + private readonly IVoucherRepository voucherRepository; + private readonly IUnitOfWork uow; + + public VoucherService(IVoucherRepository voucherRepository, IUnitOfWork uow) + { + this.voucherRepository = voucherRepository; + this.uow = uow; + } + 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 Task> DeleteVoucherAsync(Guid voucherGuid, CancellationToken token) + //{ + // throw new NotImplementedException(); + //} + + //public Task>> GetAllVouchersAsync(CancellationToken token) + //{ + // throw new NotImplementedException(); + //} + + //public Task> GetVoucherByGuidAsync(Guid voucherGuid, CancellationToken token) + //{ + // throw new NotImplementedException(); + //} + + //public Task> UpdateVoucherAsync(Guid voucherGuid, UpdateVoucherDto voucherUpdateDto, CancellationToken token) + //{ + // throw new NotImplementedException(); + //} + } +} 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..ae01033b 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,8 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi 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/VoucherRepository.cs b/Backend/src/ShoeStore.Infrastructure/Repositories/VoucherRepository.cs new file mode 100644 index 00000000..913f89cc --- /dev/null +++ b/Backend/src/ShoeStore.Infrastructure/Repositories/VoucherRepository.cs @@ -0,0 +1,19 @@ +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(); + } + } +} From ae166d71d5210f9cdf129957c3a856f729f4ae94 Mon Sep 17 00:00:00 2001 From: Le Huu Viet Hoang Date: Sun, 19 Apr 2026 23:06:07 +0700 Subject: [PATCH 03/13] chore: rename voucherRepository to repository --- .../Services/VoucherService.cs | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/Backend/src/ShoeStore.Application/Services/VoucherService.cs b/Backend/src/ShoeStore.Application/Services/VoucherService.cs index 0ebf6136..3c6cf0b0 100644 --- a/Backend/src/ShoeStore.Application/Services/VoucherService.cs +++ b/Backend/src/ShoeStore.Application/Services/VoucherService.cs @@ -4,11 +4,12 @@ using ShoeStore.Application.Interface.Common; using ShoeStore.Domain.Entities; using ShoeStore.Domain.Enum; + namespace ShoeStore.Application.Services { public class VoucherService : IVoucherService { - private readonly IVoucherRepository voucherRepository; + private readonly IVoucherRepository repository; private readonly IUnitOfWork uow; public VoucherService(IVoucherRepository voucherRepository, IUnitOfWork uow) @@ -35,30 +36,15 @@ public async Task> CreateVoucherAsync(CreateVoucherDto voucherC IsDeleted = false }; - voucherRepository.Add(voucher); + repository.Add(voucher); await uow.SaveChangesAsync(token); return Result.Created; } - //public Task> DeleteVoucherAsync(Guid voucherGuid, CancellationToken token) - //{ - // throw new NotImplementedException(); - //} - - //public Task>> GetAllVouchersAsync(CancellationToken token) - //{ - // throw new NotImplementedException(); - //} - - //public Task> GetVoucherByGuidAsync(Guid voucherGuid, CancellationToken token) - //{ - // throw new NotImplementedException(); - //} - - //public Task> UpdateVoucherAsync(Guid voucherGuid, UpdateVoucherDto voucherUpdateDto, CancellationToken token) - //{ - // throw new NotImplementedException(); - //} + public Task> UpdateVoucherAsync(Guid voucherGuid, UpdateVoucherDto voucherUpdateDto, CancellationToken token) + { + throw new NotImplementedException(); + } } } From 9d7f38803c895073cce988cb357e4dcba1dea093 Mon Sep 17 00:00:00 2001 From: Le Huu Viet Hoang Date: Mon, 20 Apr 2026 00:03:41 +0700 Subject: [PATCH 04/13] feat: adding POST api/admin/voucher --- .../Controllers/VoucherController.cs | 18 ++++++- .../DTOs/VoucherDtos/UpdateVoucherDto.cs | 24 +++++++--- .../VoucherInterface/IVoucherService.cs | 2 +- .../Services/VoucherService.cs | 47 +++++++++++++++++-- 4 files changed, 78 insertions(+), 13 deletions(-) diff --git a/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs b/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs index f529299e..75e5ec4b 100644 --- a/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs +++ b/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs @@ -13,7 +13,7 @@ namespace ShoeStore.Api.Controllers; /// Service for handling voucher logic operations. [ApiController] [Route("api/admin/vouchers")] -[Authorize(Roles = "Admin")] +// [Authorize(Roles = "Admin")] public class VoucherController(IVoucherService voucherService) : ControllerBase { /// @@ -39,6 +39,7 @@ public class VoucherController(IVoucherService voucherService) : ControllerBase [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) { @@ -52,4 +53,19 @@ public async Task CreateVoucher([FromBody] CreateVoucherDto creat details = errors })); } + + [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.Any(e => e.Type == ErrorType.NotFound) + ? NotFound(new { message = "Voucher not found" }) + : BadRequest(new + { + message = "Failed to update voucher", + details = errors + })); + } } diff --git a/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/UpdateVoucherDto.cs b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/UpdateVoucherDto.cs index 8390842b..1da41e96 100644 --- a/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/UpdateVoucherDto.cs +++ b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/UpdateVoucherDto.cs @@ -1,17 +1,27 @@ namespace ShoeStore.Application.DTOs.VoucherDtos { public class UpdateVoucherDto - { + { public string? VoucherDescription { get; set; } - public int? VoucherScope { get; set; } - public int? DiscountType { 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; } - public DateTime UpdateAt { get; set; } = DateTime.UtcNow; - public DateTime? ValidFrom { get; set; } - public DateTime? ValidTo { 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 bool? IsDeleted { get; set; } = false; public decimal? MinOrderPrice { get; set; } } } diff --git a/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs b/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs index 105ccd99..f86e57cc 100644 --- a/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs +++ b/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs @@ -7,7 +7,7 @@ public interface IVoucherService Task> CreateVoucherAsync(CreateVoucherDto voucherCreateDto, CancellationToken token); //Task> GetVoucherByGuidAsync(Guid voucherGuid, CancellationToken token); //Task>> GetAllVouchersAsync(CancellationToken token); - //Task> UpdateVoucherAsync(Guid voucherGuid, UpdateVoucherDto voucherUpdateDto, CancellationToken token); + Task> UpdateVoucherAsync(Guid voucherGuid, UpdateVoucherDto voucherUpdateDto, CancellationToken token); //Task> DeleteVoucherAsync(Guid voucherGuid, CancellationToken token ); } } diff --git a/Backend/src/ShoeStore.Application/Services/VoucherService.cs b/Backend/src/ShoeStore.Application/Services/VoucherService.cs index 3c6cf0b0..d1109cac 100644 --- a/Backend/src/ShoeStore.Application/Services/VoucherService.cs +++ b/Backend/src/ShoeStore.Application/Services/VoucherService.cs @@ -4,6 +4,7 @@ using ShoeStore.Application.Interface.Common; using ShoeStore.Domain.Entities; using ShoeStore.Domain.Enum; +using Microsoft.EntityFrameworkCore; namespace ShoeStore.Application.Services { @@ -12,9 +13,9 @@ public class VoucherService : IVoucherService private readonly IVoucherRepository repository; private readonly IUnitOfWork uow; - public VoucherService(IVoucherRepository voucherRepository, IUnitOfWork uow) + public VoucherService(IVoucherRepository repository, IUnitOfWork uow) { - this.voucherRepository = voucherRepository; + this.repository = repository; this.uow = uow; } public async Task> CreateVoucherAsync(CreateVoucherDto voucherCreateDto, CancellationToken token) @@ -42,9 +43,47 @@ public async Task> CreateVoucherAsync(CreateVoucherDto voucherC } - public Task> UpdateVoucherAsync(Guid voucherGuid, UpdateVoucherDto voucherUpdateDto, CancellationToken token) + public async Task> UpdateVoucherAsync( + Guid voucherGuid, + UpdateVoucherDto voucherUpdateDto, + CancellationToken token) { - throw new NotImplementedException(); + + var voucher = await repository + .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; + + repository.Update(voucher); + await uow.SaveChangesAsync(token); + + return Result.Updated; + } + } } From 78fe4db46ce97b5d3a52a11a290b03e55fbf1ae3 Mon Sep 17 00:00:00 2001 From: Le Huu Viet Hoang Date: Wed, 22 Apr 2026 00:45:19 +0700 Subject: [PATCH 05/13] feat: adding GET API for user to get all the voucher --- .../Controllers/VoucherController.cs | 57 +++++++- ...ponseVoucherDto.cs => DeleteVoucherDto.cs} | 4 +- .../VoucherDtos/ResponseVoucherAdminDto.cs | 29 ++++ .../VoucherDtos/ResponseVoucherUserDto.cs | 23 ++++ .../IUserVoucherRepository.cs | 10 ++ .../VoucherInterface/IUserVoucherService.cs | 10 ++ .../VoucherInterface/IVoucherService.cs | 15 +- .../Services/VoucherService.cs | 129 ++++++++++++++++-- .../DependencyInjection.cs | 2 + .../Repositories/UserVoucherRepository.cs | 20 +++ .../Repositories/VoucherRepository.cs | 1 + 11 files changed, 280 insertions(+), 20 deletions(-) rename Backend/src/ShoeStore.Application/DTOs/VoucherDtos/{ResponseVoucherDto.cs => DeleteVoucherDto.cs} (61%) create mode 100644 Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherAdminDto.cs create mode 100644 Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherUserDto.cs create mode 100644 Backend/src/ShoeStore.Application/Interface/VoucherInterface/IUserVoucherRepository.cs create mode 100644 Backend/src/ShoeStore.Application/Interface/VoucherInterface/IUserVoucherService.cs create mode 100644 Backend/src/ShoeStore.Infrastructure/Repositories/UserVoucherRepository.cs diff --git a/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs b/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs index 75e5ec4b..39156e72 100644 --- a/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs +++ b/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs @@ -14,7 +14,7 @@ namespace ShoeStore.Api.Controllers; [ApiController] [Route("api/admin/vouchers")] // [Authorize(Roles = "Admin")] -public class VoucherController(IVoucherService voucherService) : ControllerBase +public class VoucherController(IVoucherService voucherService, IUserVoucherService userVoucherService) : ControllerBase { /// /// Creates a new voucher for the store. @@ -68,4 +68,57 @@ public async Task UpdateVoucher(Guid voucherGuid, [FromBody] Upda details = errors })); } -} + + [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.Any(e => e.Type == ErrorType.NotFound) + ? NotFound(new { message = "Voucher not found" }) + : BadRequest(new + { + message = "Failed to delete voucher", + details = errors + })); + } + [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", + details = errors + })); + } + + [HttpGet] + public async Task GetVouchersForAdmin(CancellationToken token) + { + var result = await voucherService.GetVoucherForAdminAsync(token); + return result.Match( + vouchers => Ok(vouchers), + errors => BadRequest(new + { + message = "Failed to retrieve vouchers", + details = errors + })); + } + + [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 => BadRequest(new + { + message = "Failed to retrieve vouchers for user", + details = errors + })); + } +} \ No newline at end of file diff --git a/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherDto.cs b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/DeleteVoucherDto.cs similarity index 61% rename from Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherDto.cs rename to Backend/src/ShoeStore.Application/DTOs/VoucherDtos/DeleteVoucherDto.cs index 2500cf67..5a1f142e 100644 --- a/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherDto.cs +++ b/Backend/src/ShoeStore.Application/DTOs/VoucherDtos/DeleteVoucherDto.cs @@ -4,8 +4,8 @@ namespace ShoeStore.Application.DTOs.VoucherDtos { - public class ResponseVoucherDto + 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/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/IVoucherService.cs b/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs index f86e57cc..63e37b49 100644 --- a/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs +++ b/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs @@ -1,13 +1,18 @@ using ShoeStore.Application.DTOs.VoucherDtos; using ErrorOr; +using ShoeStore.Application.DTOs; namespace ShoeStore.Application.Interface.VoucherInterface { public interface IVoucherService { - Task> CreateVoucherAsync(CreateVoucherDto voucherCreateDto, CancellationToken token); - //Task> GetVoucherByGuidAsync(Guid voucherGuid, CancellationToken token); - //Task>> GetAllVouchersAsync(CancellationToken token); - Task> UpdateVoucherAsync(Guid voucherGuid, UpdateVoucherDto voucherUpdateDto, CancellationToken token); - //Task> DeleteVoucherAsync(Guid voucherGuid, CancellationToken token ); + // CREATE + Task> CreateVoucherAsync(CreateVoucherDto voucherCreateDto, CancellationToken token); + // GET + Task>> GetVoucherForAdminAsync(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 index d1109cac..5aad24e4 100644 --- a/Backend/src/ShoeStore.Application/Services/VoucherService.cs +++ b/Backend/src/ShoeStore.Application/Services/VoucherService.cs @@ -5,18 +5,21 @@ using ShoeStore.Domain.Entities; using ShoeStore.Domain.Enum; using Microsoft.EntityFrameworkCore; +using ShoeStore.Application.DTOs; namespace ShoeStore.Application.Services { - public class VoucherService : IVoucherService + public class VoucherService : IVoucherService, IUserVoucherService { - private readonly IVoucherRepository repository; + private readonly IVoucherRepository voucherRepository; private readonly IUnitOfWork uow; + private readonly IUserVoucherRepository userVoucherRepository; - public VoucherService(IVoucherRepository repository, IUnitOfWork uow) + public VoucherService(IVoucherRepository voucherRepository, IUnitOfWork uow, IUserVoucherRepository userVoucherRepository) { - this.repository = repository; + this.voucherRepository = voucherRepository; this.uow = uow; + this.userVoucherRepository = userVoucherRepository; } public async Task> CreateVoucherAsync(CreateVoucherDto voucherCreateDto, CancellationToken token) { @@ -37,19 +40,123 @@ public async Task> CreateVoucherAsync(CreateVoucherDto voucherC IsDeleted = false }; - repository.Add(voucher); + voucherRepository.Add(voucher); await uow.SaveChangesAsync(token); return Result.Created; } - public async Task> UpdateVoucherAsync( - Guid voucherGuid, - UpdateVoucherDto voucherUpdateDto, - CancellationToken token) + 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; + } - var voucher = await repository + 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>> 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(); @@ -78,7 +185,7 @@ public async Task> UpdateVoucherAsync( voucher.UpdatedAt = DateTime.UtcNow; - repository.Update(voucher); + voucherRepository.Update(voucher); await uow.SaveChangesAsync(token); return Result.Updated; diff --git a/Backend/src/ShoeStore.Infrastructure/DependencyInjection/DependencyInjection.cs b/Backend/src/ShoeStore.Infrastructure/DependencyInjection/DependencyInjection.cs index ae01033b..898da647 100644 --- a/Backend/src/ShoeStore.Infrastructure/DependencyInjection/DependencyInjection.cs +++ b/Backend/src/ShoeStore.Infrastructure/DependencyInjection/DependencyInjection.cs @@ -65,6 +65,8 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi 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 index 913f89cc..ee912222 100644 --- a/Backend/src/ShoeStore.Infrastructure/Repositories/VoucherRepository.cs +++ b/Backend/src/ShoeStore.Infrastructure/Repositories/VoucherRepository.cs @@ -2,6 +2,7 @@ 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 From 3127840e81ce35c10b5e9208779bc251f4f9949b Mon Sep 17 00:00:00 2001 From: Le Huu Viet Hoang Date: Wed, 22 Apr 2026 00:59:35 +0700 Subject: [PATCH 06/13] feat: adding GET API for all voucher for admin --- .../Controllers/VoucherController.cs | 12 +++++++ .../VoucherInterface/IVoucherService.cs | 1 + .../Services/VoucherService.cs | 34 +++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs b/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs index 39156e72..f16f55c3 100644 --- a/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs +++ b/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs @@ -121,4 +121,16 @@ public async Task GetVouchersForUser(Guid userGuid, CancellationT details = errors })); } + [HttpGet("all")] + public async Task GetAllVouchers(CancellationToken token) + { + var result = await voucherService.GetAllVouchersAsync(token); + return result.Match( + vouchers => Ok(vouchers), + errors => BadRequest(new + { + message = "Failed to retrieve all vouchers", + details = errors + })); + } } \ No newline at end of file diff --git a/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs b/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs index 63e37b49..d364bbab 100644 --- a/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs +++ b/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs @@ -9,6 +9,7 @@ public interface IVoucherService 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 diff --git a/Backend/src/ShoeStore.Application/Services/VoucherService.cs b/Backend/src/ShoeStore.Application/Services/VoucherService.cs index 5aad24e4..903fdc80 100644 --- a/Backend/src/ShoeStore.Application/Services/VoucherService.cs +++ b/Backend/src/ShoeStore.Application/Services/VoucherService.cs @@ -121,6 +121,40 @@ public async Task>> GetAllVoucherForU 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 From e047a589bb4f9c81339b7445ac446f724ef37d48 Mon Sep 17 00:00:00 2001 From: Le Huu Viet Hoang Date: Thu, 23 Apr 2026 01:57:55 +0700 Subject: [PATCH 07/13] adding the user voucher controller --- .../Controllers/UserVoucherController.cs | 25 +++++++++++++++++++ .../Controllers/VoucherController.cs | 15 +---------- 2 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs diff --git a/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs b/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs new file mode 100644 index 00000000..98b133ef --- /dev/null +++ b/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using ShoeStore.Application.Interface.VoucherInterface; + +namespace ShoeStore.Api.Controllers +{ + [ApiController] + [Route("api/user/vouchers")] + [Authorize(Roles = "User")] + public class UserVoucherController(IUserVoucherService userVoucherService) : Controller + { + [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 => BadRequest(new + { + message = "Failed to retrieve vouchers for user", + details = errors + })); + } + } +} diff --git a/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs b/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs index f16f55c3..e0ea8afe 100644 --- a/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs +++ b/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs @@ -13,7 +13,7 @@ namespace ShoeStore.Api.Controllers; /// Service for handling voucher logic operations. [ApiController] [Route("api/admin/vouchers")] -// [Authorize(Roles = "Admin")] +[Authorize(Roles = "Admin")] public class VoucherController(IVoucherService voucherService, IUserVoucherService userVoucherService) : ControllerBase { /// @@ -108,19 +108,6 @@ public async Task GetVouchersForAdmin(CancellationToken token) details = errors })); } - - [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 => BadRequest(new - { - message = "Failed to retrieve vouchers for user", - details = errors - })); - } [HttpGet("all")] public async Task GetAllVouchers(CancellationToken token) { From 21884cc54d5b255406a28ee37cd74bdf41374186 Mon Sep 17 00:00:00 2001 From: Le Huu Viet Hoang Date: Thu, 23 Apr 2026 02:04:17 +0700 Subject: [PATCH 08/13] chore: change the way coding status for api --- .../Controllers/UserVoucherController.cs | 64 ++++-- .../Controllers/VoucherController.cs | 182 +++++++++++++++--- 2 files changed, 199 insertions(+), 47 deletions(-) diff --git a/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs b/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs index 98b133ef..6a7398cc 100644 --- a/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs +++ b/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs @@ -1,25 +1,57 @@ -using Microsoft.AspNetCore.Authorization; +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 +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 { - [ApiController] - [Route("api/user/vouchers")] - [Authorize(Roles = "User")] - public class UserVoucherController(IUserVoucherService userVoucherService) : Controller + /// + /// 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) { - [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 => BadRequest(new + 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", - details = errors - })); - } + detail = errors[0].Description + }) + }); } } diff --git a/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs b/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs index e0ea8afe..54d840fb 100644 --- a/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs +++ b/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs @@ -1,6 +1,7 @@ using ErrorOr; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using ShoeStore.Application.DTOs; using ShoeStore.Application.DTOs.VoucherDtos; using ShoeStore.Application.Interface.VoucherInterface; @@ -8,13 +9,13 @@ namespace ShoeStore.Api.Controllers; /// /// Controller for managing vouchers in the system. -/// Provides endpoints for voucher creation and management (Admin only). +/// 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, IUserVoucherService userVoucherService) : ControllerBase +public class VoucherController(IVoucherService voucherService) : ControllerBase { /// /// Creates a new voucher for the store. @@ -22,11 +23,11 @@ public class VoucherController(IVoucherService voucherService, IUserVoucherServi /// /// 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 + /// - 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. @@ -35,11 +36,10 @@ public class VoucherController(IVoucherService voucherService, IUserVoucherServi /// 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(StatusCodes.Status201Created)] + [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) { @@ -50,39 +50,109 @@ public async Task CreateVoucher([FromBody] CreateVoucherDto creat errors => BadRequest(new { message = "Failed to create voucher", - details = errors + 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) + 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.Any(e => e.Type == ErrorType.NotFound) - ? NotFound(new { message = "Voucher not found" }) - : BadRequest(new + 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", - details = errors - })); + 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.Any(e => e.Type == ErrorType.NotFound) - ? NotFound(new { message = "Voucher not found" }) - : BadRequest(new + 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", - details = errors - })); + 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) { @@ -92,32 +162,82 @@ public async Task DeleteExpiredVouchers(CancellationToken token) errors => BadRequest(new { message = "Failed to delete expired vouchers", - details = errors + 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 => BadRequest(new + errors => errors[0].Code switch { - message = "Failed to retrieve vouchers", - details = errors - })); + "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 => BadRequest(new + errors => errors[0].Code switch { - message = "Failed to retrieve all vouchers", - details = errors - })); + "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 + }) + }); } -} \ No newline at end of file +} From aee09a35046e2db23fd50bbb9a28c137474a976a Mon Sep 17 00:00:00 2001 From: Le Huu Viet Hoang Date: Sun, 26 Apr 2026 00:34:40 +0700 Subject: [PATCH 09/13] fix --- .../Controllers/UserVoucherController.cs | 12 ++++++++++-- .../ShoeStore.Application/Services/VoucherService.cs | 4 ++-- .../DependencyInjection/DependencyInjection.cs | 4 ---- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs b/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs index 6a7398cc..09e44392 100644 --- a/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs +++ b/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs @@ -1,3 +1,4 @@ +using System.Security.Claims; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using ShoeStore.Application.DTOs; @@ -34,9 +35,16 @@ public class UserVoucherController(IUserVoucherService userVoucherService) : Con [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) + [HttpGet("user")] + public async Task GetVouchersForUser(CancellationToken token) { + var userGuidString = User.FindFirstValue(ClaimTypes.NameIdentifier); + + if (string.IsNullOrEmpty(userGuidString)) + return Unauthorized(); + + var userGuid = Guid.Parse(userGuidString); + var result = await userVoucherService.GetAllVoucherForUserAsync(userGuid, token); return result.Match( vouchers => Ok(vouchers), diff --git a/Backend/src/ShoeStore.Application/Services/VoucherService.cs b/Backend/src/ShoeStore.Application/Services/VoucherService.cs index 903fdc80..34747f6e 100644 --- a/Backend/src/ShoeStore.Application/Services/VoucherService.cs +++ b/Backend/src/ShoeStore.Application/Services/VoucherService.cs @@ -9,7 +9,7 @@ namespace ShoeStore.Application.Services { - public class VoucherService : IVoucherService, IUserVoucherService + public class VoucherService : IVoucherService { private readonly IVoucherRepository voucherRepository; private readonly IUnitOfWork uow; @@ -105,7 +105,7 @@ public async Task>> GetAllVoucherForU }) .ToListAsync(token); - if(vouchers == null || !vouchers.Any()) + if(!vouchers.Any()) { return Error.NotFound( "NO_VOUCHERS_FOUND", diff --git a/Backend/src/ShoeStore.Infrastructure/DependencyInjection/DependencyInjection.cs b/Backend/src/ShoeStore.Infrastructure/DependencyInjection/DependencyInjection.cs index 829159e9..539604b6 100644 --- a/Backend/src/ShoeStore.Infrastructure/DependencyInjection/DependencyInjection.cs +++ b/Backend/src/ShoeStore.Infrastructure/DependencyInjection/DependencyInjection.cs @@ -63,14 +63,10 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi services.AddScoped(); services.AddScoped(); services.AddScoped(); -<<<<<<< HEAD services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); -======= services.AddScoped(); ->>>>>>> 1ef445073c48ac01c2f8c4961f37165bef0c2623 return services; } } \ No newline at end of file From 080532bd5e62f5ba940cc0f910c2daddf8f14c84 Mon Sep 17 00:00:00 2001 From: Le Huu Viet Hoang Date: Sun, 26 Apr 2026 02:10:30 +0700 Subject: [PATCH 10/13] feat: notifaction when admin created voucher --- .../Controllers/UserVoucherController.cs | 4 +- .../Controllers/VoucherController.cs | 28 ++++++--- .../UserInterface/IUserRepository.cs | 1 + .../VoucherInterface/IVoucherService.cs | 4 +- .../Services/VoucherService.cs | 60 +++++++++++++++++-- .../Repositories/UserRepository.cs | 5 ++ 6 files changed, 85 insertions(+), 17 deletions(-) diff --git a/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs b/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs index 09e44392..d9816669 100644 --- a/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs +++ b/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs @@ -38,13 +38,11 @@ public class UserVoucherController(IUserVoucherService userVoucherService) : Con [HttpGet("user")] public async Task GetVouchersForUser(CancellationToken token) { - var userGuidString = User.FindFirstValue(ClaimTypes.NameIdentifier); - + var userGuidString = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; if (string.IsNullOrEmpty(userGuidString)) return Unauthorized(); var userGuid = Guid.Parse(userGuidString); - var result = await userVoucherService.GetAllVoucherForUserAsync(userGuid, token); return result.Match( vouchers => Ok(vouchers), diff --git a/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs b/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs index 54d840fb..f5c0b80b 100644 --- a/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs +++ b/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs @@ -1,3 +1,4 @@ +using System.Security.Claims; using ErrorOr; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -45,13 +46,26 @@ public async Task CreateVoucher([FromBody] CreateVoucherDto creat { 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 - })); + return await result.MatchAsync( + async _ => + { + var adminEmail = User.FindFirstValue(ClaimTypes.Email) ?? "admin@example.com"; + + await voucherService.NotifyUserAboutNewVoucherAsync( + adminEmail: adminEmail, + voucherName: createVoucherDto.VoucherName ?? "New Discount", + validTo: createVoucherDto.ValidTo ?? DateTime.UtcNow, + token: token + ); + + return Created("", new { message = "Voucher created and users notified" }); + }, + errors => Task.FromResult(BadRequest(new + { + message = "Failed to create voucher", + detail = errors[0].Description + })) + ); } /// diff --git a/Backend/src/ShoeStore.Application/Interface/UserInterface/IUserRepository.cs b/Backend/src/ShoeStore.Application/Interface/UserInterface/IUserRepository.cs index 1249c338..6e52842e 100644 --- a/Backend/src/ShoeStore.Application/Interface/UserInterface/IUserRepository.cs +++ b/Backend/src/ShoeStore.Application/Interface/UserInterface/IUserRepository.cs @@ -10,4 +10,5 @@ public interface IUserRepository : IGenericRepository Task GetUserByEmailAsync(string email, CancellationToken token); Task GetUserByPublicIdAsync(Guid publicId, CancellationToken token); + IQueryable GetAllUsers(); } \ No newline at end of file diff --git a/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs b/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs index d364bbab..75c20050 100644 --- a/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs +++ b/Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs @@ -13,7 +13,9 @@ public interface IVoucherService // UPDATE Task> UpdateVoucherAsync(Guid voucherGuid, UpdateVoucherDto voucherUpdateDto, CancellationToken token); // DELETE - Task> DeleteVoucherByGuidAsync(Guid voucherGuid, CancellationToken token ); + Task> DeleteVoucherByGuidAsync(Guid voucherGuid, CancellationToken token); Task> DeleteVoucherExpireAsync(CancellationToken token); + // NOTIFY + Task> NotifyUserAboutNewVoucherAsync(string adminEmail, string voucherName, DateTime validTo, CancellationToken token); } } diff --git a/Backend/src/ShoeStore.Application/Services/VoucherService.cs b/Backend/src/ShoeStore.Application/Services/VoucherService.cs index 34747f6e..d8af6abf 100644 --- a/Backend/src/ShoeStore.Application/Services/VoucherService.cs +++ b/Backend/src/ShoeStore.Application/Services/VoucherService.cs @@ -1,11 +1,14 @@ -using ShoeStore.Application.Interface.VoucherInterface; -using ShoeStore.Application.DTOs.VoucherDtos; +using System.ComponentModel.DataAnnotations; using ErrorOr; +using Microsoft.EntityFrameworkCore; +using ShoeStore.Application.DTOs; +using ShoeStore.Application.DTOs.VoucherDtos; +using ShoeStore.Application.Interface; using ShoeStore.Application.Interface.Common; +using ShoeStore.Application.Interface.Notification; +using ShoeStore.Application.Interface.VoucherInterface; using ShoeStore.Domain.Entities; using ShoeStore.Domain.Enum; -using Microsoft.EntityFrameworkCore; -using ShoeStore.Application.DTOs; namespace ShoeStore.Application.Services { @@ -14,12 +17,16 @@ public class VoucherService : IVoucherService private readonly IVoucherRepository voucherRepository; private readonly IUnitOfWork uow; private readonly IUserVoucherRepository userVoucherRepository; + private readonly IEmailService emailService; + private readonly IUserRepository userRepository; - public VoucherService(IVoucherRepository voucherRepository, IUnitOfWork uow, IUserVoucherRepository userVoucherRepository) + public VoucherService(IVoucherRepository voucherRepository, IUnitOfWork uow, IUserVoucherRepository userVoucherRepository, IEmailService emailService, IUserRepository userRepository) { this.voucherRepository = voucherRepository; this.uow = uow; this.userVoucherRepository = userVoucherRepository; + this.emailService = emailService; + this.userRepository = userRepository; } public async Task> CreateVoucherAsync(CreateVoucherDto voucherCreateDto, CancellationToken token) { @@ -43,9 +50,9 @@ public async Task> CreateVoucherAsync(CreateVoucherDto voucherC voucherRepository.Add(voucher); await uow.SaveChangesAsync(token); return Result.Created; - } + public async Task> DeleteVoucherByGuidAsync(Guid voucherGuid, CancellationToken token) { var voucher = await voucherRepository @@ -188,6 +195,47 @@ public async Task>> GetVoucherForAdm return pageResult; } + public async Task> NotifyUserAboutNewVoucherAsync(string adminEmail, string voucherName, DateTime validTo, CancellationToken token) + { + var users = await userRepository + .GetAllUsers() + .Where(u => u.Email != adminEmail) + .ToListAsync(token); + + foreach (var user in users) + { + if (user.Email != adminEmail) + { + return Error.NotFound( + "USER_NOT_FOUND", + "The user with the specified email does not exist." + ); + } + string emailBody = $@" + Hi {user.UserName}, + + Great news! A new voucher has been added to your account: + + 🎁 {voucherName.ToUpper()} + 📅 Valid until: {validTo:MMMM dd, yyyy} + + Check your wallet and start shopping now to enjoy your discount! + + Best regards, + Shoe Store Team"; + + await emailService.SendEmailAsync( + from: adminEmail, + to: user.Email, + subject: "🎁 New Voucher Received!", + body: emailBody, + token: token + ); + } + return Result.Success; + } + + public async Task> UpdateVoucherAsync(Guid voucherGuid, UpdateVoucherDto voucherUpdateDto, CancellationToken token) { var voucher = await voucherRepository diff --git a/Backend/src/ShoeStore.Infrastructure/Repositories/UserRepository.cs b/Backend/src/ShoeStore.Infrastructure/Repositories/UserRepository.cs index 4abf6c6c..62c27f78 100644 --- a/Backend/src/ShoeStore.Infrastructure/Repositories/UserRepository.cs +++ b/Backend/src/ShoeStore.Infrastructure/Repositories/UserRepository.cs @@ -23,4 +23,9 @@ public async Task IsEmailExistAsync(string email, CancellationToken token) .Include(u => u.UserVouchers) .FirstOrDefaultAsync(x => x.PublicId == publicId, token); } + + public IQueryable GetAllUsers() + { + return DbSet.AsNoTracking(); + } } \ No newline at end of file From 6722c6cbe7093589dc5a9c8cc2a30109f75953d4 Mon Sep 17 00:00:00 2001 From: Le Huu Viet Hoang Date: Sun, 26 Apr 2026 02:38:54 +0700 Subject: [PATCH 11/13] fix --- .../Services/VoucherService.cs | 48 +++++++++++-------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/Backend/src/ShoeStore.Application/Services/VoucherService.cs b/Backend/src/ShoeStore.Application/Services/VoucherService.cs index d8af6abf..47f79635 100644 --- a/Backend/src/ShoeStore.Application/Services/VoucherService.cs +++ b/Backend/src/ShoeStore.Application/Services/VoucherService.cs @@ -197,21 +197,23 @@ public async Task>> GetVoucherForAdm public async Task> NotifyUserAboutNewVoucherAsync(string adminEmail, string voucherName, DateTime validTo, CancellationToken token) { - var users = await userRepository - .GetAllUsers() - .Where(u => u.Email != adminEmail) - .ToListAsync(token); - - foreach (var user in users) + try { - if (user.Email != adminEmail) + var users = await userRepository + .GetAllUsers() + .Where(u => u.Email != adminEmail) + .ToListAsync(token); + + foreach (var user in users) { - return Error.NotFound( - "USER_NOT_FOUND", - "The user with the specified email does not exist." - ); - } - string emailBody = $@" + if (user.Email != adminEmail) + { + return Error.NotFound( + "USER_NOT_FOUND", + "The user with the specified email does not exist." + ); + } + string emailBody = $@" Hi {user.UserName}, Great news! A new voucher has been added to your account: @@ -224,12 +226,20 @@ Check your wallet and start shopping now to enjoy your discount! Best regards, Shoe Store Team"; - await emailService.SendEmailAsync( - from: adminEmail, - to: user.Email, - subject: "🎁 New Voucher Received!", - body: emailBody, - token: token + await emailService.SendEmailAsync( + from: adminEmail, + to: user.Email, + subject: "🎁 New Voucher Received!", + body: emailBody, + token: token + ); + } + } + catch (Exception ex) + { + return Error.Failure( + "EMAIL_SENDING_FAILED", + $"Failed to send email notifications: {ex.Message}" ); } return Result.Success; From 185012d3ade0556789af5df5bab462152ab3f233 Mon Sep 17 00:00:00 2001 From: Le Huu Viet Hoang Date: Mon, 27 Apr 2026 01:16:20 +0700 Subject: [PATCH 12/13] fix: change update voucher from FromBody to FromFrom --- Backend/src/ShoeStore.Api/Controllers/VoucherController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs b/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs index 9162ef83..03352193 100644 --- a/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs +++ b/Backend/src/ShoeStore.Api/Controllers/VoucherController.cs @@ -15,7 +15,7 @@ namespace ShoeStore.Api.Controllers; /// Service for handling voucher logic operations. [ApiController] [Route("api/admin/vouchers")] -// [Authorize(Roles = "Admin")] +[Authorize(Roles = "Admin")] public class VoucherController(IVoucherService voucherService) : ControllerBase { /// From 46e52652ee42ecea0ddbe2482eb39d9ae37b3a6e Mon Sep 17 00:00:00 2001 From: Tran Quang Ha Date: Mon, 27 Apr 2026 08:11:18 +0700 Subject: [PATCH 13/13] Apply suggestion from @coderabbitai[bot] Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../src/ShoeStore.Api/Controllers/UserVoucherController.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs b/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs index d9816669..65518862 100644 --- a/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs +++ b/Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs @@ -38,11 +38,10 @@ public class UserVoucherController(IUserVoucherService userVoucherService) : Con [HttpGet("user")] public async Task GetVouchersForUser(CancellationToken token) { - var userGuidString = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; - if (string.IsNullOrEmpty(userGuidString)) + var userGuidString = User.FindFirstValue(ClaimTypes.NameIdentifier); + if (string.IsNullOrEmpty(userGuidString) || !Guid.TryParse(userGuidString, out var userGuid)) return Unauthorized(); - var userGuid = Guid.Parse(userGuidString); var result = await userVoucherService.GetAllVoucherForUserAsync(userGuid, token); return result.Match( vouchers => Ok(vouchers),