Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
71 changes: 71 additions & 0 deletions Backend/src/ShoeStore.Api/Controllers/VoucherController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using ErrorOr;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
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 and management (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
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{
/// <summary>
/// Creates a new voucher for the store.
/// </summary>
/// <remarks>
/// 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
/// </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(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",
details = errors
}));
}
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.


[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.Any(e => e.Type == ErrorType.NotFound)
? NotFound(new { message = "Voucher not found" })
: BadRequest(new
{
message = "Failed to update voucher",
details = errors
}));
}
}
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 +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 ResponseVoucherDto

Check warning on line 7 in Backend/src/ShoeStore.Application/DTOs/VoucherDtos/ResponseVoucherDto.cs

View workflow job for this annotation

GitHub Actions / Backend Build and Analyze

Remove this empty class, write its code or make it an "interface".
{

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
namespace ShoeStore.Application.DTOs.VoucherDtos
{
public class UpdateVoucherDto
{
public string? VoucherDescription { get; set; }
public decimal? Discount { get; set; }
public int VoucherScope { get; set; } = 1; // Default to AllProducts
public int DiscountType { get; set; } = 1; // Default to Percentage
public decimal MaxPriceDiscount { get; set; }
private DateTime? _validFrom;
public DateTime? ValidFrom
{
get => _validFrom;
set => _validFrom = value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null;
}

private DateTime? _validTo;
public DateTime? ValidTo
{
get => _validTo;
set => _validTo = value.HasValue ? DateTime.SpecifyKind(value.Value, DateTimeKind.Utc) : null;
}
public int? MaxUsagePerUser { get; set; }
public int? TotalQuantity { get; set; }
public decimal? MinOrderPrice { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using ShoeStore.Application.Interface.Common;
using ShoeStore.Domain.Entities;

namespace ShoeStore.Application.Interface.VoucherInterface
{
public interface IVoucherRepository : IGenericRepository<Voucher, int>
{
IQueryable<Voucher> GetAllVouchers();
IQueryable<Voucher> GetVoucherByGuid(Guid voucherGuid);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using ShoeStore.Application.DTOs.VoucherDtos;
using ErrorOr;
namespace ShoeStore.Application.Interface.VoucherInterface
{
public interface IVoucherService
{
Task<ErrorOr<Created>> CreateVoucherAsync(CreateVoucherDto voucherCreateDto, CancellationToken token);
//Task<ErrorOr<VoucherDto>> GetVoucherByGuidAsync(Guid voucherGuid, CancellationToken token);

Check warning on line 8 in Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs

View workflow job for this annotation

GitHub Actions / Backend Build and Analyze

Remove this commented out code.
//Task<ErrorOr<IEnumerable<VoucherDto>>> GetAllVouchersAsync(CancellationToken token);
Task<ErrorOr<Updated>> UpdateVoucherAsync(Guid voucherGuid, UpdateVoucherDto voucherUpdateDto, CancellationToken token);
//Task<ErrorOr<Deleted>> DeleteVoucherAsync(Guid voucherGuid, CancellationToken token );

Check warning on line 11 in Backend/src/ShoeStore.Application/Interface/VoucherInterface/IVoucherService.cs

View workflow job for this annotation

GitHub Actions / Backend Build and Analyze

Remove this commented out code.
}
}
89 changes: 89 additions & 0 deletions Backend/src/ShoeStore.Application/Services/VoucherService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
using ShoeStore.Application.Interface.VoucherInterface;
using ShoeStore.Application.DTOs.VoucherDtos;
using ErrorOr;
using ShoeStore.Application.Interface.Common;
using ShoeStore.Domain.Entities;
using ShoeStore.Domain.Enum;
using Microsoft.EntityFrameworkCore;

namespace ShoeStore.Application.Services
{
public class VoucherService : IVoucherService
{
private readonly IVoucherRepository repository;
private readonly IUnitOfWork uow;

public VoucherService(IVoucherRepository repository, IUnitOfWork uow)
{
this.repository = repository;
this.uow = uow;
}
public async Task<ErrorOr<Created>> 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,
Comment on lines +40 to +41
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

🏁 Script executed:

#!/bin/bash
fd CreateVoucherDtoValidation.cs --exec cat {}
fd CreateVoucherDto.cs --exec cat {}

Repository: tqha1011/ShoeStore

Length of output: 2141


Unvalidated intenum casts persist invalid enum values.

Verified: CreateVoucherDtoValidation contains no validation for VoucherScope or DiscountType. Both fields lack IsInEnum() rules, allowing any int to silently cast to an enum on lines 27–28, corrupting voucher records.

Add to CreateVoucherDtoValidation:

RuleFor(x => x.VoucherScope)
    .IsInEnum().WithMessage("Invalid VoucherScope value.");

RuleFor(x => x.DiscountType)
    .IsInEnum().WithMessage("Invalid DiscountType value.");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Backend/src/ShoeStore.Application/Services/VoucherService.cs` around lines 27
- 28, The CreateVoucherDtoValidation is missing enum validation, so unvalidated
int values are being cast into enums in VoucherService when assigning
VoucherScope and DiscountType; update CreateVoucherDtoValidation to add
IsInEnum() rules for the VoucherScope and DiscountType properties (e.g.,
RuleFor(x => x.VoucherScope).IsInEnum().WithMessage(...); RuleFor(x =>
x.DiscountType).IsInEnum().WithMessage(...)) to ensure invalid ints are rejected
before VoucherService performs the casts.

MaxPriceDiscount = voucherCreateDto.MaxPriceDiscount,
ValidFrom = voucherCreateDto.ValidFrom,
ValidTo = voucherCreateDto.ValidTo,
MaxUsagePerUser = voucherCreateDto.MaxUsagePerUser,
TotalQuantity = voucherCreateDto.TotalQuantity ?? 0,
MinOrderPrice = voucherCreateDto.MinOrderPrice ?? 0,
IsDeleted = false
};

repository.Add(voucher);
await uow.SaveChangesAsync(token);
return Result.Created;

}

public async Task<ErrorOr<Updated>> UpdateVoucherAsync(
Guid voucherGuid,
UpdateVoucherDto voucherUpdateDto,
CancellationToken token)
{

var voucher = await repository
.GetVoucherByGuid(voucherGuid)
.FirstOrDefaultAsync();
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify no-token EF async query calls in C# files.
# Expected: voucher query uses FirstOrDefaultAsync(token) or an equivalent CancellationToken overload.
rg -nP --type cs -C2 '\.FirstOrDefaultAsync\s*\(\s*\)'

Repository: tqha1011/ShoeStore

Length of output: 545


🏁 Script executed:

cat -n Backend/src/ShoeStore.Application/Services/VoucherService.cs | head -100

Repository: tqha1011/ShoeStore

Length of output: 3990


🏁 Script executed:

rg -n 'FirstOrDefaultAsync\s*\(\s*\)' Backend/src/ShoeStore.Application/Services/VoucherService.cs

Repository: tqha1011/ShoeStore

Length of output: 104


Pass the cancellation token to the database query.

The UpdateVoucherAsync method accepts a CancellationToken token parameter but doesn't pass it to FirstOrDefaultAsync() on line 54. Without the token, the database read operation cannot be cancelled, even though the save operation on line 82 correctly uses it.

Proposed fix
             var voucher = await repository
                 .GetVoucherByGuid(voucherGuid)
-                .FirstOrDefaultAsync();
+                .FirstOrDefaultAsync(token);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Backend/src/ShoeStore.Application/Services/VoucherService.cs` around lines 52
- 54, In UpdateVoucherAsync, the database read call using
repository.GetVoucherByGuid(...).FirstOrDefaultAsync() does not pass the
CancellationToken parameter, so the read cannot be cancelled; update the call to
pass the provided CancellationToken token into FirstOrDefaultAsync(token) (or
the appropriate overload) so the cancellation token is honored during the query
before you later call SaveChangesAsync(token).


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;

Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
voucher.UpdatedAt = DateTime.UtcNow;

repository.Update(voucher);
await uow.SaveChangesAsync(token);

return Result.Updated;

}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using FluentValidation;
using ShoeStore.Application.DTOs.VoucherDtos;

namespace ShoeStore.Application.Validations.VoucherValidation;

public class CreateVoucherDtoValidation : AbstractValidator<CreateVoucherDto>
{
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");
Comment on lines +10 to +19
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

🏁 Script executed:

#!/bin/bash
# Description: Find the domain/entity definitions and usages for voucher scope/type/value fields.
# Expected: Use the discovered allowed values to replace the placeholder Must(...) checks above.

rg -n -C3 --type=cs '\b(VoucherScope|DiscountType|MaxPriceDiscount|MinOrderPrice|MaxUsagePerUser)\b'

Repository: tqha1011/ShoeStore

Length of output: 50374


🏁 Script executed:

fd -n "CreateVoucherDto" --type f

Repository: tqha1011/ShoeStore

Length of output: 292


🏁 Script executed:

fd "CreateVoucherDto" --type f

Repository: tqha1011/ShoeStore

Length of output: 226


🏁 Script executed:

cat -n "Backend/src/ShoeStore.Application/DTOs/VoucherDtos/CreateVoucherDto.cs"

Repository: tqha1011/ShoeStore

Length of output: 1334


🏁 Script executed:

fd "Voucher" --type f | grep -E "(Entity|Domain|Model)" | head -20

Repository: tqha1011/ShoeStore

Length of output: 268


🏁 Script executed:

cat -n "Backend/src/ShoeStore.Domain/Entities/Voucher.cs"

Repository: tqha1011/ShoeStore

Length of output: 1532


🏁 Script executed:

cat -n "Backend/src/ShoeStore.Domain/Enum/VoucherScope.cs"

Repository: tqha1011/ShoeStore

Length of output: 207


🏁 Script executed:

fd "DiscountType" --type f

Repository: tqha1011/ShoeStore

Length of output: 111


🏁 Script executed:

cat -n "Backend/src/ShoeStore.Domain/Enum/DiscountType.cs"

Repository: tqha1011/ShoeStore

Length of output: 213


🏁 Script executed:

cat -n "Backend/src/ShoeStore.Application/Validations/VoucherValidation/CreateVoucherDtoValidation.cs"

Repository: tqha1011/ShoeStore

Length of output: 1253


🏁 Script executed:

cat -n "Backend/src/ShoeStore.Application/Validations/VoucherValidation/CreateVoucherDtoValidation.cs" | head -30

Repository: tqha1011/ShoeStore

Length of output: 1253


Add missing validation rules for voucher enum fields and economic constraints.

The validation is incomplete. VoucherScope and DiscountType accept arbitrary integers, percentage discounts can exceed 100%, and negative values are allowed for MaxPriceDiscount, MinOrderPrice, and MaxUsagePerUser. These should be rejected at the DTO boundary.

🛡️ Add missing validation rules
+        RuleFor(x => x.VoucherScope)
+            .Must(value => value is 1 or 2)
+            .WithMessage("Voucher scope must be 1 (Product) or 2 (Shipping)");
+
+        RuleFor(x => x.DiscountType)
+            .Must(value => value is 1 or 2)
+            .WithMessage("Discount type must be 1 (FixedAmount) or 2 (Percentage)");
+
         RuleFor(x => x.Discount)
             .NotNull().WithMessage("Discount is required")
             .GreaterThanOrEqualTo(0).WithMessage("Discount must be greater than or equal to 0");
+
+        RuleFor(x => x.Discount)
+            .LessThanOrEqualTo(100).WithMessage("Percentage discount must not exceed 100")
+            .When(x => x.DiscountType == 2);
+
+        RuleFor(x => x.MaxPriceDiscount)
+            .GreaterThanOrEqualTo(0).WithMessage("Max price discount must be greater than or equal to 0");
+
+        RuleFor(x => x.MaxUsagePerUser)
+            .GreaterThan(0).WithMessage("Max usage per user must be greater than 0")
+            .When(x => x.MaxUsagePerUser.HasValue);
+
+        RuleFor(x => x.MinOrderPrice)
+            .GreaterThanOrEqualTo(0).WithMessage("Minimum order price must be greater than or equal to 0")
+            .When(x => x.MinOrderPrice.HasValue);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Backend/src/ShoeStore.Application/Validations/VoucherValidation/CreateVoucherDtoValidation.cs`
around lines 10 - 19, Add validations for enum and numeric constraints: ensure
VoucherScope and DiscountType use IsInEnum() (e.g., RuleFor(x =>
x.VoucherScope).IsInEnum().WithMessage(...), RuleFor(x =>
x.DiscountType).IsInEnum().WithMessage(...)); add a conditional rule that when
DiscountType == DiscountType.Percentage the Discount value is <= 100 (RuleFor(x
=> x.Discount).LessThanOrEqualTo(100).When(x => x.DiscountType ==
DiscountType.Percentage).WithMessage(...)); and enforce non-negative constraints
for MaxPriceDiscount, MinOrderPrice and MaxUsagePerUser (e.g., RuleFor(x =>
x.MaxPriceDiscount).GreaterThanOrEqualTo(0)..., RuleFor(x =>
x.MinOrderPrice).GreaterThanOrEqualTo(0)..., RuleFor(x =>
x.MaxUsagePerUser).GreaterThanOrEqualTo(0)...). Use the
CreateVoucherDtoValidation class and the property names VoucherScope,
DiscountType, Discount, MaxPriceDiscount, MinOrderPrice, and MaxUsagePerUser to
locate where to add these rules.


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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -62,6 +63,8 @@ public static IServiceCollection AddInfrastructure(this IServiceCollection servi
services.AddScoped<IColorRepository, ColorRepository>();
services.AddScoped<ISizeRepository, SizeRepository>();
services.AddScoped<ICategoryRepository, CategoryRepository>();
services.AddScoped<IVoucherRepository, VoucherRepository>();
services.AddScoped<IVoucherService, VoucherService>();
return services;
}
}
Original file line number Diff line number Diff line change
@@ -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<Voucher, int>(context), IVoucherRepository
{
public IQueryable<Voucher> GetAllVouchers()
{
return context.Vouchers.AsNoTracking();
}

public IQueryable<Voucher> GetVoucherByGuid(Guid voucherGuid)
{
return context.Vouchers.Where(v => v.PublicId == voucherGuid).AsNoTracking();
}
}
}
Loading