Skip to content
Merged
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
62 changes: 62 additions & 0 deletions Backend/src/ShoeStore.Api/Controllers/UserVoucherController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
using System.Security.Claims;
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")]
public async Task<IActionResult> GetVouchersForUser(CancellationToken token)
{
var userGuidString = User.FindFirstValue(ClaimTypes.NameIdentifier);
if (string.IsNullOrEmpty(userGuidString) || !Guid.TryParse(userGuidString, out var userGuid))
return Unauthorized();

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
})
});
}
}
220 changes: 220 additions & 0 deletions Backend/src/ShoeStore.Api/Controllers/VoucherController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
using System.Security.Claims;
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 await result.MatchAsync<IActionResult>(
async _ =>
{
var adminEmail = User.FindFirstValue(ClaimTypes.Email) ?? "[email protected]";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Hardcoded [email protected] fallback is a smell.

If the authenticated principal lacks an Email claim, the email will be sent from: "[email protected]", which is RFC 2606 reserved and will be rejected or flagged as spam by most SMTP relays, and may also leak into outgoing email headers. Prefer failing fast with a 400/401 (Admin must have an email claim) or sourcing the sender from configuration rather than hardcoding a placeholder address into the request flow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Backend/src/ShoeStore.Api/Controllers/VoucherController.cs` at line 52, The
current fallback of var adminEmail = User.FindFirstValue(ClaimTypes.Email) ??
"[email protected]" in VoucherController is unsafe; replace this by validating
the presence of the Email claim and returning an appropriate error (e.g.,
400/401) if missing, or read a configured sender address instead of a hardcoded
placeholder. Locate the adminEmail assignment in the VoucherController, remove
the "[email protected]" default, and implement either: (a) a guard that checks
User.FindFirstValue(ClaimTypes.Email) and returns BadRequest/Unauthorized with a
clear message if null/empty, or (b) resolve the sender from configuration
(IConfiguration or options) and use that when the claim is absent, ensuring no
hardcoded reserved address is ever used.


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<IActionResult>(BadRequest(new
{
message = "Failed to create voucher",
detail = errors[0].Description
}))
);
}
Comment on lines +49 to +69
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Notification failure swallows successful voucher creation and returns 400.

MatchAsync only enters the success branch after CreateVoucherAsync has already persisted the voucher (per VoucherService.CreateVoucherAsync calling uow.SaveChangesAsync before returning Result.Created). If NotifyUserAboutNewVoucherAsync then returns an ErrorOr failure (e.g., SMTP failure → EMAIL_SENDING_FAILED), its result is silently ignored here — the awaited Task<ErrorOr<Success>> is discarded — so the API still responds 201 Created. Conversely, if the notify call throws an exception, the whole request bubbles up as 500, even though the voucher was committed. Either way, callers cannot reliably distinguish "voucher created" from "voucher created + notification failed".

Consider:

  • Inspecting the result of NotifyUserAboutNewVoucherAsync and surfacing a partial-success response (e.g., 201 with a warning), or
  • Moving notification dispatch out-of-band (background job / outbox) so HTTP success reflects only the persisted state.

Additionally, Created("", ...) emits an empty Location header; prefer CreatedAtAction(nameof(GetVouchersForAdmin), ...) or returning the new voucher's GUID-based URL.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Backend/src/ShoeStore.Api/Controllers/VoucherController.cs` around lines 49 -
69, The success branch of result.MatchAsync ignores the outcome of
NotifyUserAboutNewVoucherAsync causing notification failures to be swallowed or
to surface as 500 even though CreateVoucherAsync already persisted the voucher;
update the success branch to await and inspect the ErrorOr result from
NotifyUserAboutNewVoucherAsync (handle both Error and Success cases), return
CreatedAtAction(nameof(GetVouchersForAdmin), ...) or CreatedAtAction with the
new voucher id and include a partial-success warning when notification failed,
and ensure any exceptions from NotifyUserAboutNewVoucherAsync are caught and
translated into a non-failing warning response (or, alternatively, move
notification dispatch out-of-band via an outbox/background job) so HTTP 201
reflects the persisted voucher state rather than the delivery outcome.


/// <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, [FromForm] 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
}));
}
Comment on lines +166 to +181
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

NO_EXPIRED_VOUCHERS is mapped to 400 instead of 404.

VoucherService.DeleteVoucherExpireAsync returns Error.NotFound("NO_EXPIRED_VOUCHERS", ...) (see lines 85–88 of VoucherService.cs), but this handler funnels every error into BadRequest, and the [ProducesResponseType] attributes don't advertise 404 either. This is inconsistent with the other handlers (UpdateVoucher, DeleteVoucher, GetVouchersForAdmin, GetAllVouchers) which switch on errors[0].Code to surface NotFound.

Proposed fix
     [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)]
     [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
+            errors => errors[0].Code switch
             {
-                message = "Failed to delete expired vouchers",
-                detail = errors[0].Description
-            }));
+                "NO_EXPIRED_VOUCHERS" => NotFound(new
+                {
+                    message = "No expired vouchers found",
+                    detail = errors[0].Description
+                }),
+                _ => BadRequest(new
+                {
+                    message = "Failed to delete expired vouchers",
+                    detail = errors[0].Description
+                })
+            });
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Backend/src/ShoeStore.Api/Controllers/VoucherController.cs` around lines 166
- 181, The DeleteExpiredVouchers handler currently maps all errors to
BadRequest; change it to inspect the returned errors (from
voucherService.DeleteVoucherExpireAsync) and when errors[0].Code ==
"NO_EXPIRED_VOUCHERS" return NotFound(...) instead of BadRequest, mirroring
other handlers (e.g.,
UpdateVoucher/DeleteVoucher/GetVouchersForAdmin/GetAllVouchers) and add a
corresponding [ProducesResponseType(typeof(object),
StatusCodes.Status404NotFound)] attribute to the DeleteExpiredVouchers action;
ensure other error cases still produce BadRequest as before.


/// <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
})
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

using ShoeStore.Domain.Enum;

namespace ShoeStore.Application.DTOs.VoucherDtos
{
public class CreateVoucherDto
{
public string? VoucherName { get; set; }
public string? VoucherDescription { get; set; }
public decimal? Discount { get; set; }
public VoucherScope VoucherScope { get; set; } = VoucherScope.Product;
public DiscountType DiscountType { get; set; } = DiscountType.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 +15 to +26
Copy link
Copy Markdown
Contributor

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,30 @@
using System;
using System.Collections.Generic;
using System.Text;

namespace ShoeStore.Application.DTOs.VoucherDtos
{
public class ResponseVoucherAdminDto
{
public Guid VoucherGuid { get; set; }
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,24 @@

namespace ShoeStore.Application.DTOs.VoucherDtos
{
public class ResponseVoucherUserDto
{
public Guid VoucherGuid { get; set; }
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;
}
Comment on lines +10 to +22
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

SpecifyKind relabels rather than converts — silent time-zone corruption risk.

DateTime.SpecifyKind does not convert the value; it only changes the Kind flag. If a caller ever assigns a DateTime whose Kind is Local or Unspecified but represents local/zoned time, the setter will silently mark it as UTC without adjusting the underlying value, leading to incorrect ValidFrom/ValidTo returned to users.

This is currently safe only because the entity values are assumed to already be in UTC. Consider ToUniversalTime() (or asserting the Kind) if there's any chance the source isn't UTC.

♻️ Suggested adjustment
-            set => _validFrom = value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null;
+            set => _validFrom = value.HasValue
+                ? (value.Value.Kind == DateTimeKind.Utc
+                    ? value.Value
+                    : value.Value.ToUniversalTime())
+                : null;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherUserDto.cs`
around lines 9 - 21, The setters for ValidFrom/ValidTo use DateTime.SpecifyKind
which only relabels Kind and can silently corrupt times; update the setters for
_validFrom and _validTo to ensure the incoming DateTime is converted to UTC
instead of merely changing its Kind — e.g., if value.HasValue, convert
value.Value to UTC (using ToUniversalTime or by checking Kind and calling
ToUniversalTime when Kind != Utc) and then store the converted UTC DateTime;
replace the use of DateTime.SpecifyKind with a proper UTC conversion flow for
both ValidFrom and ValidTo.

}
}
Loading
Loading