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
6 changes: 3 additions & 3 deletions Dentizone.Application/AutoMapper/Answers/AnswerProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ public class AnswerProfile : Profile
{
public AnswerProfile()
{
CreateMap<Dentizone.Application.DTOs.Q_A.AnswerDTO.CreateAnswerDto, Dentizone.Domain.Entity.Answer>()
CreateMap<DTOs.Q_A.AnswerDTO.CreateAnswerDto, Domain.Entity.Answer>()
.ForMember(dest => dest.Id, opt => opt.MapFrom(src => Guid.NewGuid().ToString()))
.ForMember(dest => dest.CreatedAt, opt => opt.MapFrom(src => DateTime.UtcNow))
.ReverseMap();
CreateMap<Dentizone.Application.DTOs.Q_A.AnswerDTO.UpdateAnswerDto, Dentizone.Domain.Entity.Answer>()
CreateMap<DTOs.Q_A.AnswerDTO.UpdateAnswerDto, Domain.Entity.Answer>()
.ForMember(dest => dest.Text, opt => opt.MapFrom(src => src.Text))
.ForMember(dest => dest.UpdatedAt, opt => opt.MapFrom(src => DateTime.UtcNow))
.ReverseMap();
CreateMap<Dentizone.Application.DTOs.Q_A.AnswerDTO.AnswerViewDto, Dentizone.Domain.Entity.Answer>()
CreateMap<DTOs.Q_A.AnswerDTO.AnswerViewDto, Domain.Entity.Answer>()
.ReverseMap();
}
}
Expand Down
4 changes: 2 additions & 2 deletions Dentizone.Application/DTOs/User/CreateAppUser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ namespace Dentizone.Application.DTOs.User
public class CreateAppUser
{
public string
Id
{ get; set; }
Id { get; set; }

public string FullName { get; set; }
public string Email { get; set; }
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.

🛠️ Refactor suggestion

Add validation attributes to the Email property.

The Email property lacks validation attributes that would ensure data integrity and provide clear validation feedback.

+using System.ComponentModel.DataAnnotations;
+
 namespace Dentizone.Application.DTOs.User
 {
     public class CreateAppUser
     {
         public string Id { get; set; }
         public string FullName { get; set; }
+        [Required]
+        [EmailAddress]
         public string Email { get; set; }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public string Email { get; set; }
using System.ComponentModel.DataAnnotations;
namespace Dentizone.Application.DTOs.User
{
public class CreateAppUser
{
public string Id { get; set; }
public string FullName { get; set; }
[Required]
[EmailAddress]
public string Email { get; set; }
}
}
🤖 Prompt for AI Agents
In Dentizone.Application/DTOs/User/CreateAppUser.cs at line 11, the Email
property is missing validation attributes. Add appropriate data annotation
attributes such as [Required] and [EmailAddress] to the Email property to
enforce that it is not null or empty and that it contains a valid email format,
ensuring data integrity and clear validation feedback.

public string Username { get; set; }
public string UniversityId { get; set; }
public int AcademicYear { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion Dentizone.Application/Interfaces/IUploadService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ public interface IUploadService
public Task<AssetDto> UploadImageAsync(IFormFile file, string userId);
public Task<AssetDto> FindAssetById(string id);

public Task<AssetDto> DeleteAssetById(string id, string userId);
Task DeleteAssetById(string id);
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.

💡 Verification agent

🧩 Analysis chain

Verify that this breaking change has been properly handled across the codebase.

The method signature change removes the userId parameter and changes the return type from Task<AssetDto> to Task. This is a breaking change that could impact:

  1. All implementations of IUploadService
  2. All callers expecting the deleted asset to be returned
  3. Any logging or audit functionality that relied on the returned asset details

Please run the following script to verify all implementations and usages have been updated:


🏁 Script executed:

#!/bin/bash
# Description: Verify all implementations and usages of DeleteAssetById have been updated

echo "=== Checking for implementations of IUploadService ==="
ast-grep --pattern 'class $_ : $$$IUploadService$$$'

echo -e "\n=== Checking for DeleteAssetById method implementations ==="
ast-grep --pattern 'DeleteAssetById($_) {
  $$$
}'

echo -e "\n=== Checking for calls to DeleteAssetById ==="
rg -A 3 'DeleteAssetById\(' --type cs

echo -e "\n=== Checking if any code still expects AssetDto return type ==="
rg -B 2 -A 2 'DeleteAssetById.*AssetDto' --type cs

Length of output: 1934


Controller call must be updated to match new signature

The interface and service have been updated to Task DeleteAssetById(string id), but the controller still calls the old overload with two parameters. Update the caller accordingly.

• File: Dentizone.Presentaion/Controllers/UploadController.cs
– Change the service call to use the single-parameter signature
– Remove the userId argument

Recommended diff:

-            await uploadService.DeleteAssetById(id, userId);
+            await uploadService.DeleteAssetById(id);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Task DeleteAssetById(string id);
await uploadService.DeleteAssetById(id);
🤖 Prompt for AI Agents
In Dentizone.Presentaion/Controllers/UploadController.cs, locate the calls to
DeleteAssetById that pass two parameters and update them to call the service
method with only the single string id parameter, removing the userId argument to
match the updated interface signature in
Dentizone.Application/Interfaces/IUploadService.cs at line 11.

}
}
2 changes: 1 addition & 1 deletion Dentizone.Application/Interfaces/Order/IOrderService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public interface IOrderService
Task<string> CreateOrderAsync(CreateOrderDto createOrderDto, string buyerId);
Task<OrderViewDto> GetOrderByIdAsync(string orderId, string buyerId);
Task<List<OrderViewDto>> GetOrdersByBuyerAsync(string buyerId);
Task<OrderViewDto?> CancelOrderAsync(string orderId, string userId);
Task<OrderViewDto?> CancelOrderAsync(string orderId);
Task CompleteOrder(string orderId);
Task<PagedResultDto<OrderViewDto>> GetOrders(int page, FilterOrderDto filters);

Expand Down
19 changes: 18 additions & 1 deletion Dentizone.Application/Services/AssetService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
using Dentizone.Domain.Entity;
using Dentizone.Domain.Exceptions;
using Dentizone.Domain.Interfaces.Repositories;
using Microsoft.AspNetCore.Http;

namespace Dentizone.Application.Services
{
public class AssetService(IAssetRepository assetRepository, IMapper mapper) : IAssetService
public class AssetService(IAssetRepository assetRepository, IMapper mapper, IHttpContextAccessor contextAccessor)
: BaseService(contextAccessor), IAssetService
{
public async Task<AssetDto> CreateAssetAsync(CreateAssetDto assetDto)
{
Expand All @@ -30,14 +32,29 @@ public async Task<AssetDto> UpdateAssetAsync(string id, UpdateAssetDto assetDto)
{
var existingAsset = await assetRepository.GetByIdAsync(id) ??
throw new NotFoundException($"Asset with id {id} not found");

await AuthorizeAdminOrOwnerAsync(existingAsset.Id);

var u = mapper.Map(assetDto, existingAsset);
var updatedAsset = await assetRepository.UpdateAsync(u);
return mapper.Map<AssetDto>(updatedAsset);
}

public async Task DeleteAssetAsync(string assetId)
{
await AuthorizeAdminOrOwnerAsync(assetId);
await assetRepository.DeleteByIdAsync(assetId);
}

protected override async Task<string> GetOwnerIdAsync(string resourceId)
{
var asset = await assetRepository.GetByIdAsync(resourceId);
if (asset == null)
{
throw new NotFoundException($"Asset with id {resourceId} not found");
}

return asset.UserId;
}
}
}
69 changes: 69 additions & 0 deletions Dentizone.Application/Services/BaseService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Dentizone.Application/Services/BaseService.cs

using System.Security.Claims;
using Dentizone.Domain.Enums;
using Dentizone.Domain.Exceptions;
using Microsoft.AspNetCore.Http;

namespace Dentizone.Application.Services;

public abstract class BaseService
{
private readonly IHttpContextAccessor _httpContextAccessor;

protected BaseService(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}

/// <summary>
/// Checks if the current user has the ADMIN role.
/// </summary>
protected bool IsAdmin()
{
var userRole = _httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.Role);
return Enum.TryParse<UserRoles>(userRole, out var role) && role == UserRoles.ADMIN;
}

/// <summary>
/// Ensures the current user is either an Admin or the owner of the specified resource.
/// Throws UnauthorizedAccessException if the check fails.
/// </summary>
/// <param name="resourceId">The unique identifier of the resource to check.</param>
protected async Task AuthorizeAdminOrOwnerAsync(string resourceId)
{
// Admins are always authorized.
if (IsAdmin())
{
return;
}

var currentUserId = _httpContextAccessor.HttpContext?.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (string.IsNullOrEmpty(currentUserId))
{
throw new UnauthorizedAccessException("Cannot verify user. No user is authenticated.");
}

// Get the owner ID from the concrete service implementation.
var ownerId = await GetOwnerIdAsync(resourceId);

if (string.IsNullOrEmpty(ownerId))
{
throw new NotFoundException("Could not determine the owner of the resource.");
}

// If the user is not the owner, they are not authorized.
if (ownerId != currentUserId)
{
throw new UnauthorizedAccessException(
"You do not have permission to perform this action on this resource.");
}
}

/// <summary>
/// When implemented in a derived class, this method retrieves the owner's ID for a given resource.
/// </summary>
/// <param name="resourceId">The ID of the resource.</param>
/// <returns>A Task that represents the asynchronous operation, containing the owner's user ID.</returns>
protected abstract Task<string> GetOwnerIdAsync(string resourceId);
}
22 changes: 16 additions & 6 deletions Dentizone.Application/Services/OrderService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using Dentizone.Domain.Interfaces.Repositories;
using Dentizone.Infrastructure;
using System.Linq.Expressions;
using Microsoft.AspNetCore.Http;

namespace Dentizone.Application.Services
{
Expand All @@ -27,10 +28,11 @@ internal class OrderService(
IAuthService authService,
IPaymentService paymentService,
ICartService cartService,
IHttpContextAccessor accessor,
AppDbContext dbContext)
: IOrderService
: BaseService(accessor), IOrderService
{
public async Task<OrderViewDto?> CancelOrderAsync(string orderId, string userId)
public async Task<OrderViewDto?> CancelOrderAsync(string orderId)
{
var order = await orderRepository.FindBy(o => o.Id == orderId,
[o => o.OrderStatuses, o => o.OrderItems]);
Expand All @@ -40,10 +42,7 @@ internal class OrderService(
throw new NotFoundException("Order not found.");
}

if (order.BuyerId != userId)
{
throw new UnauthorizedAccessException("You are not allowed to cancel this order.");
}
await AuthorizeAdminOrOwnerAsync(orderId);

if (order.OrderStatuses.Any(os => os.Status == OrderStatues.Cancelled))
{
Expand Down Expand Up @@ -290,5 +289,16 @@ public async Task<IEnumerable<Order>> GetReviewedOrdersByUserId(string userId)
);
return orders.Items;
}

protected override async Task<string> GetOwnerIdAsync(string resourceId)
{
var order = await orderRepository.GetByIdAsync(resourceId);
if (order == null)
{
throw new NotFoundException($"Order with id {resourceId} not found");
}

return order.BuyerId;
}
}
}
22 changes: 12 additions & 10 deletions Dentizone.Application/Services/Payment/PaymentService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,15 @@ public async Task CreateSaleTransaction(string paymentId, string walletId, decim

public async Task<PaymentView> ConfirmPaymentAsync(string orderId)
{
await using var DatabaseTransaction = await db.Database.BeginTransactionAsync();
await using var databaseTransaction = await db.Database.BeginTransactionAsync();

try
{
// 1. Find The Payment // 2. Get Sales Transaction by PaymentId
var payment =
await
repo.FindBy(p => p.OrderId == orderId && p.Status == PaymentStatus.Pending,
includes: [p => p.SalesTransactions]);
includes: [p => p.SalesTransactions]);
// 3. Update Payment Status to Confirmed
if (payment == null)
{
Expand All @@ -109,7 +109,8 @@ public async Task<PaymentView> ConfirmPaymentAsync(string orderId)
if (transaction.Status != SaleStatus.Pending)
{
throw new
InvalidOperationException($"Sale transaction of id {transaction.Id} is not in pending status.");
InvalidOperationException(
$"Sale transaction of id {transaction.Id} is not in pending status.");
}

transaction.Status = SaleStatus.Completed;
Expand All @@ -128,27 +129,27 @@ public async Task<PaymentView> ConfirmPaymentAsync(string orderId)
}

// Commit the transaction
await DatabaseTransaction.CommitAsync();
await databaseTransaction.CommitAsync();

return mapper.Map<PaymentView>(updatedPayment);
}
catch (Exception)
{
// Rollback the transaction in case of an error
await DatabaseTransaction.RollbackAsync();
await databaseTransaction.RollbackAsync();

throw;
}
}

public async Task CancelPaymentByOrderId(string orderId)
{
await using var DatabaseTransaction = await db.Database.BeginTransactionAsync();
await using var databaseTransaction = await db.Database.BeginTransactionAsync();
try
{
// Find the payment by order ID
var payment = await repo.FindBy(p => p.OrderId == orderId && p.Status == PaymentStatus.Pending,
includes: [p => p.SalesTransactions]);
includes: [p => p.SalesTransactions]);
if (payment == null)
{
throw new NotFoundException("Payment not found.");
Expand All @@ -168,7 +169,8 @@ public async Task CancelPaymentByOrderId(string orderId)
if (transaction.Status != SaleStatus.Pending)
{
throw new
InvalidOperationException($"Sale transaction of id {transaction.Id} is not in pending status.");
InvalidOperationException(
$"Sale transaction of id {transaction.Id} is not in pending status.");
}

transaction.Status = SaleStatus.Failed;
Expand All @@ -177,12 +179,12 @@ public async Task CancelPaymentByOrderId(string orderId)


// Commit the transaction
await DatabaseTransaction.CommitAsync();
await databaseTransaction.CommitAsync();
}
catch (Exception)
{
// Rollback the transaction in case of an error
await DatabaseTransaction.RollbackAsync();
await databaseTransaction.RollbackAsync();
throw;
}
}
Expand Down
Loading
Loading