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
55 changes: 55 additions & 0 deletions src/Application/Features/Message/Report/ReportMessageCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System.Text.Json.Serialization;
using Application.Common;
using Domain.Common;
using Domain.Enums;

namespace Application.Features.Message.Report;

/// <summary>
/// Command để report tin nhắn
/// Pattern: Command Pattern (CQRS)
/// </summary>
public sealed record ReportMessageCommand : ICommand<Result<ReportMessageResponse>>
{
/// <summary>
/// ID của tin nhắn cần report
/// </summary>
[JsonIgnore]
public Guid MessageId { get; init; }

/// <summary>
/// Danh mục report
/// </summary>
public required ReportCategory Category { get; init; }

/// <summary>
/// Lý do chi tiết (tùy chọn)
/// </summary>
public string? Reason { get; init; }
}

/// <summary>
/// Response sau khi report tin nhắn
/// </summary>
public sealed record ReportMessageResponse
{
/// <summary>
/// ID của report
/// </summary>
public required Guid ReportId { get; init; }

/// <summary>
/// ID của tin nhắn được report
/// </summary>
public required Guid MessageId { get; init; }

/// <summary>
/// Danh mục report
/// </summary>
public required ReportCategory Category { get; init; }

/// <summary>
/// Thời gian tạo report
/// </summary>
public required DateTimeOffset CreatedAt { get; init; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using Application.Common;
using Application.Interfaces.Repositories;
using Application.Interfaces.Services.Auth;
using Domain.Common;
using Domain.Entities;
using Domain.Enums;

namespace Application.Features.Message.Report;

/// <summary>
/// Handler xử lý ReportMessageCommand
/// Pattern: Command Handler Pattern + Template Method Pattern
/// </summary>
public sealed class ReportMessageCommandHandler(
ICurrentUserService currentUserService,
IMessageRepository messageRepository,
IReportRepository reportRepository) : ICommandHandler<ReportMessageCommand, Result<ReportMessageResponse>>
{
private readonly ICurrentUserService _currentUserService = currentUserService
?? throw new ArgumentNullException(nameof(currentUserService));
private readonly IMessageRepository _messageRepository = messageRepository
?? throw new ArgumentNullException(nameof(messageRepository));
private readonly IReportRepository _reportRepository = reportRepository
?? throw new ArgumentNullException(nameof(reportRepository));

public async Task<Result<ReportMessageResponse>> Handle(ReportMessageCommand request, CancellationToken cancellationToken)
{
// Step 1: Validate user authentication
var userId = _currentUserService.UserId;
if (userId is null)
{
return Result.Failure<ReportMessageResponse>(
Error.Unauthorized("User.Unauthenticated", "User is not authenticated"));
}

// Step 2: Get message
var message = await _messageRepository.GetByIdAsync(request.MessageId, cancellationToken);
if (message is null || message.IsDeleted)
{
return Result.Failure<ReportMessageResponse>(
Error.NotFound("Message.NotFound", "Message not found"));
}

// Step 3: Validate that user cannot report their own message
if (message.SenderId != userId.Value)
{
return Result.Failure<ReportMessageResponse>(
Error.Validation("Report.NotOwnMessage", "You cannot report not your own message"));
}

// Step 4: Check if user already reported this message
var existingReports = await _reportRepository.GetByMessageIdAsync(request.MessageId, cancellationToken);
if (existingReports.Any(r => r.ReporterId == userId.Value))
{
return Result.Failure<ReportMessageResponse>(
Error.Validation("Report.AlreadyReported", "You have already reported this message"));
}

// Step 5: Create report
var report = new Domain.Entities.Report
{
MessageId = request.MessageId,
ReporterId = userId.Value,
Category = request.Category,
Reason = request.Reason,
Status = "pending"
};

await _reportRepository.AddAsync(report, cancellationToken);

// Step 6: Return response
return Result.Success(new ReportMessageResponse
{
ReportId = report.Id,
MessageId = report.MessageId,
Category = report.Category,
CreatedAt = report.CreatedAt
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using FluentValidation;
using Domain.Enums;

namespace Application.Features.Message.Report;

/// <summary>
/// Validator cho ReportMessageCommand
/// </summary>
public sealed class ReportMessageCommandValidator : AbstractValidator<ReportMessageCommand>
{
public ReportMessageCommandValidator()
{
RuleFor(x => x.Category)
.IsInEnum()
.WithMessage("Danh mục report không hợp lệ");

RuleFor(x => x.Reason)
.MaximumLength(1000)
.WithMessage("Lý do chi tiết không được vượt quá 1000 ký tự")
.When(x => !string.IsNullOrWhiteSpace(x.Reason));
}
}
22 changes: 22 additions & 0 deletions src/Application/Interfaces/Repositories/IReportRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Application.Common.Models;
using Domain.Entities;

namespace Application.Interfaces.Repositories;

/// <summary>
/// Interface for Report repository to handle data operations.
/// </summary>
public interface IReportRepository
{
Task<PaginatedResult<Report>> GetListAsync(
PaginationRequest paginationRequest,
CancellationToken cancellationToken = default);
Task AddAsync(Report report, CancellationToken cancellationToken = default);
Task<Report?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default);
Task<IReadOnlyList<Report>> GetByMessageIdAsync(
Guid messageId,
CancellationToken cancellationToken = default);
Task<IReadOnlyList<Report>> GetByReporterIdAsync(
Guid reporterId,
CancellationToken cancellationToken = default);
}
2 changes: 2 additions & 0 deletions src/Domain/Entities/Message.cs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ public string GetPreview(int maxLength = 100)
// Navigation properties
public ThinkingActivity? ThinkingActivity { get; set; }

public ICollection<Report> Reports { get; set; } = new List<Report>();

/// <summary>
/// Start a thinking activity for this AI message
/// </summary>
Expand Down
33 changes: 33 additions & 0 deletions src/Domain/Entities/Report.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Domain.Common;
using Domain.Enums;

namespace Domain.Entities;

/// <summary>
/// Report entity for reporting inappropriate messages
/// </summary>
public sealed class Report : BaseEntity
{
public required Guid MessageId { get; set; }

public required Guid ReporterId { get; set; }

/// <summary>
/// Category of the report
/// </summary>
public required ReportCategory Category { get; set; }

/// <summary>
/// Detailed reason for reporting
/// </summary>
public string? Reason { get; set; }

/// <summary>
/// Status of the report (pending, reviewed, resolved, etc.)
/// </summary>
public string Status { get; set; } = "pending";

public Message Message { get; set; } = null!;

public User Reporter { get; set; } = null!;
}
37 changes: 37 additions & 0 deletions src/Domain/Enums/ReportCategory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.ComponentModel;

namespace Domain.Enums;

/// <summary>
/// Các danh mục báo cáo tin nhắn để cải thiện chất lượng trợ lý pháp lý
/// </summary>
public enum ReportCategory
{
/// <summary>
/// Nội dung không phù hợp với chủ đề pháp lý hoặc vi phạm tiêu chuẩn
/// Ví dụ: Nội dung không liên quan, quảng cáo, hoặc nội dung không phù hợp với đối tượng người dùng
/// </summary>
[Description("Nội dung không phù hợp")]
InappropriateContent,

/// <summary>
/// Thông tin pháp lý sai lệch, không chính xác hoặc gây hiểu lầm
/// Ví dụ: Tư vấn pháp lý không đúng với quy định hiện hành
/// </summary>
[Description("Thông tin sai lệch")]
IncorrectInformation,

/// <summary>
/// Lỗi kỹ thuật, vấn đề về hiển thị hoặc chức năng của ứng dụng
/// Ví dụ: Tin nhắn không hiển thị đúng, lỗi định dạng, hoặc vấn đề về hiệu suất
/// </summary>
[Description("Vấn đề kỹ thuật")]
TechnicalIssue,

/// <summary>
/// Các vấn đề khác không thuộc các danh mục trên
/// Vui lòng mô tả chi tiết trong phần lý do
/// </summary>
[Description("Khác")]
Other
}
66 changes: 66 additions & 0 deletions src/Infrastructure/Data/Configurations/ReportConfiguration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Domain.Entities;
using Domain.Enums;
using Infrastructure.Data.Contexts;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Infrastructure.Data.Configurations;

/// <summary>
/// Entity configuration for Report
/// </summary>
public sealed class ReportConfiguration : IEntityTypeConfiguration<Report>
{
public void Configure(EntityTypeBuilder<Report> builder)
{
builder.ToTable("Reports", Schemas.Default);

// Primary Key
builder.HasKey(r => r.Id);

// Properties
builder.Property(r => r.MessageId)
.IsRequired();

builder.Property(r => r.ReporterId)
.IsRequired();

builder.Property(r => r.Category)
.IsRequired()
.HasConversion<string>();

builder.Property(r => r.Reason)
.HasMaxLength(1000);

builder.Property(r => r.Status)
.IsRequired()
.HasMaxLength(50)
.HasDefaultValue("pending");

builder.Property(r => r.CreatedAt)
.IsRequired();

builder.Property(r => r.UpdatedAt);

// Relationships
builder.HasOne(r => r.Message)
.WithMany(m => m.Reports)
.HasForeignKey(r => r.MessageId)
.OnDelete(DeleteBehavior.Cascade);

builder.HasOne(r => r.Reporter)
.WithMany()
.HasForeignKey(r => r.ReporterId)
.OnDelete(DeleteBehavior.Restrict);

// Indexes
builder.HasIndex(r => r.MessageId);
builder.HasIndex(r => r.ReporterId);
builder.HasIndex(r => r.Category);
builder.HasIndex(r => r.Status);
builder.HasIndex(r => r.CreatedAt);

// Query Filter - Only show reports for non-deleted messages from non-deleted conversations and non-deleted users
builder.HasQueryFilter(r => !r.Message.IsDeleted && !r.Message.Conversation.IsDeleted && !r.Message.Sender.IsDeleted && !r.Reporter.IsDeleted);
}
}
1 change: 1 addition & 0 deletions src/Infrastructure/Data/Contexts/DataContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public DataContext(DbContextOptions<DataContext> options) : base(options)
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
public DbSet<ThinkingActivity> Activities => Set<ThinkingActivity>();
public DbSet<Thought> Thoughts => Set<Thought>();
public DbSet<Report> Reports => Set<Report>();

protected override void OnModelCreating(ModelBuilder builder)
{
Expand Down
Loading