-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/voucher #200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature/voucher #200
Changes from all commits
36f76fe
3d26f35
1ffec96
ae166d7
9d7f388
f7f19ab
78fe4db
3127840
e047a58
21884cc
732cf47
aee09a3
080532b
6722c6c
8d77607
185012d
46e5265
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| }) | ||
| }); | ||
| } | ||
| } |
| 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]"; | ||
|
|
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Notification failure swallows successful voucher creation and returns 400.
Consider:
Additionally, 🤖 Prompt for AI Agents |
||
|
|
||
| /// <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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 |
||
|
|
||
| /// <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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 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 fRepository: tqha1011/ShoeStore Length of output: 134 🏁 Script executed: # Search for DateTime.SpecifyKind usage across the codebase
rg "SpecifyKind" -t cs -C 3Repository: 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 Lines 16 and 23 use 🛠️ 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 |
||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is currently safe only because the entity values are assumed to already be in UTC. Consider ♻️ 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 |
||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hardcoded
[email protected]fallback is a smell.If the authenticated principal lacks an
Emailclaim, the email will be sentfrom: "[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 a400/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