Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Controller for managing user-specific voucher operations.
/// Provides endpoints for users to retrieve their own vouchers.
/// </summary>
/// <param name="userVoucherService">Service for handling user-voucher relationship operations.</param>
[ApiController]
[Route("api/user/vouchers")]
[Authorize(Roles = "User")]
public class UserVoucherController(IUserVoucherService userVoucherService) : ControllerBase
{
/// <summary>
/// Retrieves all active vouchers associated with a specific user.
/// </summary>
/// <remarks>
/// Requires User role authorization.
/// Returns a list of vouchers that are currently valid and assigned to the user.
/// </remarks>
/// <param name="userGuid">The unique identifier (GUID) of the user whose vouchers are being retrieved.</param>
/// <param name="token">Cancellation token for the request.</param>
/// <response code="200">Vouchers retrieved successfully.</response>
/// <response code="401">Unauthorized; user must be authenticated with User role.</response>
/// <response code="404">Not found; no vouchers found for the specified user.</response>
/// <response code="500">Internal server error; an unexpected error occurred.</response>
/// <returns>An action result containing a paginated list of user vouchers on success, or an error response.</returns>
[ProducesResponseType(typeof(PageResult<ResponseVoucherUserDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
[HttpGet("user/{userGuid}")]
public async Task<IActionResult> GetVouchersForUser(Guid userGuid, CancellationToken token)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cái này k cần phải là tham số, lấy từ jwt token dùng find first value

{
var result = await userVoucherService.GetAllVoucherForUserAsync(userGuid, token);
return result.Match<IActionResult>(
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
})
});
}
}
243 changes: 243 additions & 0 deletions Backend/src/ShoeStore.Api/Controllers/VoucherController.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Controller for managing vouchers in the system.
/// Provides endpoints for voucher creation, retrieval, update, and deletion (Admin only).
/// </summary>
/// <param name="voucherService">Service for handling voucher logic operations.</param>
[ApiController]
[Route("api/admin/vouchers")]
[Authorize(Roles = "Admin")]
public class VoucherController(IVoucherService voucherService) : ControllerBase
{
/// <summary>
/// Creates a new voucher for the store.
/// </summary>
/// <remarks>
/// Requires Admin role authorization.
/// The request body should include:
/// - <c>VoucherName</c>: Name of the voucher
/// - <c>Discount</c>: Value of the discount
/// - <c>DiscountType</c>: Type of discount (Percentage/FixedAmount)
/// - <c>TotalQuantity</c>: Number of vouchers available
/// - <c>ValidFrom/ValidTo</c>: Expiration dates
/// </remarks>
/// <param name="createVoucherDto">Data transfer object containing voucher creation details.</param>
/// <param name="token">Cancellation token for the request.</param>
/// <response code="201">Voucher created successfully.</response>
/// <response code="400">Bad request; invalid voucher data provided.</response>
/// <response code="401">Unauthorized; user must be authenticated with Admin role.</response>
/// <response code="500">Internal server error; an unexpected error occurred.</response>
/// <returns>An action result with status 201 (Created) on success, or an error response.</returns>
[ProducesResponseType(typeof(object), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(object), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(object), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
[HttpPost]
public async Task<IActionResult> CreateVoucher([FromBody] CreateVoucherDto createVoucherDto, CancellationToken token)
{
var result = await voucherService.CreateVoucherAsync(createVoucherDto, token);

return result.Match<IActionResult>(
_ => Created("", new { message = "Voucher created successfully" }),
errors => BadRequest(new
{
message = "Failed to create voucher",
detail = errors[0].Description
}));
}

/// <summary>
/// Updates an existing voucher's details.
/// </summary>
/// <remarks>
/// Requires Admin role authorization.
/// Updates the specified voucher with the provided information.
/// </remarks>
/// <param name="voucherGuid">The unique identifier (GUID) of the voucher to update.</param>
/// <param name="updateVoucherDto">Data transfer object containing updated voucher details.</param>
/// <param name="token">Cancellation token for the request.</param>
/// <response code="200">Voucher updated successfully.</response>
/// <response code="400">Bad request; invalid update data provided.</response>
/// <response code="401">Unauthorized; user must be authenticated with Admin role.</response>
/// <response code="404">Not found; the voucher with the specified ID does not exist.</response>
/// <response code="500">Internal server error; an unexpected error occurred.</response>
/// <returns>An action result with status 200 (OK) on success, or an error response.</returns>
[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<IActionResult> UpdateVoucher(Guid voucherGuid, [FromBody] UpdateVoucherDto updateVoucherDto,
CancellationToken token)
{
var result = await voucherService.UpdateVoucherAsync(voucherGuid, updateVoucherDto, token);
return result.Match<IActionResult>(
_ => 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
})
});
}

/// <summary>
/// Deletes a specific voucher from the system (soft delete).
/// </summary>
/// <remarks>
/// Requires Admin role authorization.
/// Performs a soft delete by marking the voucher as deleted.
/// </remarks>
/// <param name="voucherGuid">The unique identifier (GUID) of the voucher to delete.</param>
/// <param name="token">Cancellation token for the request.</param>
/// <response code="200">Voucher deleted successfully.</response>
/// <response code="401">Unauthorized; user must be authenticated with Admin role.</response>
/// <response code="404">Not found; the voucher with the specified ID does not exist.</response>
/// <response code="500">Internal server error; an unexpected error occurred.</response>
/// <returns>An action result with status 200 (OK) on success, or an error response.</returns>
[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<IActionResult> DeleteVoucher(Guid voucherGuid, CancellationToken token)
{
var result = await voucherService.DeleteVoucherByGuidAsync(voucherGuid, token);
return result.Match<IActionResult>(
_ => 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
})
});
}

/// <summary>
/// Deletes all expired vouchers from the system (soft delete).
/// </summary>
/// <remarks>
/// Requires Admin role authorization.
/// Identifies and soft deletes all vouchers whose expiration date has passed.
/// </remarks>
/// <param name="token">Cancellation token for the request.</param>
/// <response code="200">Expired vouchers deleted successfully.</response>
/// <response code="400">Bad request; failed to delete expired vouchers or no expired vouchers found.</response>
/// <response code="401">Unauthorized; user must be authenticated with Admin role.</response>
/// <response code="500">Internal server error; an unexpected error occurred.</response>
/// <returns>An action result with status 200 (OK) on success, or an error response.</returns>
[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<IActionResult> DeleteExpiredVouchers(CancellationToken token)
{
var result = await voucherService.DeleteVoucherExpireAsync(token);
return result.Match<IActionResult>(
_ => Ok(new { message = "Expired vouchers deleted successfully" }),
errors => BadRequest(new
{
message = "Failed to delete expired vouchers",
detail = errors[0].Description
}));
}

/// <summary>
/// Retrieves a paginated list of vouchers for administrative purposes.
/// </summary>
/// <remarks>
/// Requires Admin role authorization.
/// Provides a list of vouchers with detailed information relevant for administrators.
/// </remarks>
/// <param name="token">Cancellation token for the request.</param>
/// <response code="200">Vouchers retrieved successfully.</response>
/// <response code="401">Unauthorized; user must be authenticated with Admin role.</response>
/// <response code="404">Not found; no vouchers found in the system.</response>
/// <response code="500">Internal server error; an unexpected error occurred.</response>
/// <returns>An action result containing a paginated list of vouchers on success, or an error response.</returns>
[ProducesResponseType(typeof(PageResult<ResponseVoucherAdminDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
[HttpGet]
public async Task<IActionResult> GetVouchersForAdmin(CancellationToken token)
{
var result = await voucherService.GetVoucherForAdminAsync(token);
return result.Match<IActionResult>(
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
})
});
}

/// <summary>
/// Retrieves all active and non-deleted vouchers in the system.
/// </summary>
/// <remarks>
/// Requires Admin role authorization.
/// </remarks>
/// <param name="token">Cancellation token for the request.</param>
/// <response code="200">All vouchers retrieved successfully.</response>
/// <response code="401">Unauthorized; user must be authenticated with Admin role.</response>
/// <response code="404">Not found; no vouchers found in the system.</response>
/// <response code="500">Internal server error; an unexpected error occurred.</response>
/// <returns>An action result containing a list of vouchers on success, or an error response.</returns>
[ProducesResponseType(typeof(PageResult<ResponseVoucherAdminDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(object), StatusCodes.Status401Unauthorized)]
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
[HttpGet("all")]
public async Task<IActionResult> GetAllVouchers(CancellationToken token)
{
var result = await voucherService.GetAllVouchersAsync(token);
return result.Match<IActionResult>(
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
})
});
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Comment on lines +13 to +24
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Microsoft .NET DateTime.SpecifyKind documentation: does SpecifyKind convert the DateTime value to UTC or only set the Kind property?

💡 Result:

DateTime.SpecifyKind does not convert the DateTime value to UTC. It only sets the Kind property while keeping the same number of ticks (time value) unchanged. The official documentation explicitly states: "The returned DateTime value does not represent the same instant in time as the value parameter, and SpecifyKind is not a time zone conversion method. Instead, it leaves the time specified by the value parameter unchanged, and sets the Kind property to kind." For time zone conversions, use methods like ToUniversalTime or ToLocalTime.

Citations:


🏁 Script executed:

# First, locate and inspect the CreateVoucherDto.cs file
find . -name "CreateVoucherDto.cs" -type f

Repository: tqha1011/ShoeStore

Length of output: 134


🏁 Script executed:

# Search for DateTime.SpecifyKind usage across the codebase
rg "SpecifyKind" -t cs -C 3

Repository: tqha1011/ShoeStore

Length of output: 5521


🏁 Script executed:

# Search for similar DateTime assignment patterns in DTOs
rg "DateTime\.SpecifyKind|DateTimeKind\.Utc" -t cs --glob "**/*Dto.cs"

Repository: tqha1011/ShoeStore

Length of output: 419


Use ToUniversalTime() to convert non-UTC times, not SpecifyKind().

Lines 16 and 23 use DateTime.SpecifyKind(), which only sets metadata without converting the timestamp. If clients send local or offset-based times, they get relabeled as UTC without any actual conversion, causing vouchers to expire at the wrong instant.

🛠️ Proper UTC normalization
+        private static DateTime? NormalizeUtc(DateTime? value)
+        {
+            if (!value.HasValue)
+            {
+                return null;
+            }
+
+            return value.Value.Kind == DateTimeKind.Unspecified
+                ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc)
+                : value.Value.ToUniversalTime();
+        }
+
         private DateTime? _validFrom;
         public DateTime? ValidFrom
         {
             get => _validFrom;
-            set => _validFrom = value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null;
+            set => _validFrom = NormalizeUtc(value);
         }
 
         private DateTime? _validTo;
         public DateTime? ValidTo
         {
             get => _validTo;
-            set => _validTo = value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null;
+            set => _validTo = NormalizeUtc(value);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Backend/src/ShoeStore.Application/DTOs/VoucherDtos/CreateVoucherDto.cs`
around lines 13 - 24, The setters for ValidFrom and ValidTo currently use
DateTime.SpecifyKind which only labels the value as UTC without converting the
instant; update the setters in CreateVoucherDto (properties ValidFrom/ValidTo,
backing fields _validFrom/_validTo) to convert incoming non-null DateTime values
to UTC using ToUniversalTime() (e.g., set _validFrom = value.HasValue ?
value.Value.ToUniversalTime() : null) so timestamps are actually normalized to
UTC while preserving null handling.

public int? MaxUsagePerUser { get; set; }
public int? TotalQuantity { get; set; }
public decimal? MinOrderPrice { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Loading
Loading