diff --git a/services/mission-service-net/src/MissionService.API/Application/Commands/TaskCommandHandlers.cs b/services/mission-service-net/src/MissionService.API/Application/Commands/TaskCommandHandlers.cs
new file mode 100644
index 00000000..6be719d4
--- /dev/null
+++ b/services/mission-service-net/src/MissionService.API/Application/Commands/TaskCommandHandlers.cs
@@ -0,0 +1,230 @@
+using MediatR;
+using MissionService.Domain.AggregatesModel.MissionAggregate;
+using MissionService.Domain.AggregatesModel.TaskAggregate;
+using TaskStatus = MissionService.Domain.AggregatesModel.TaskAggregate.TaskStatus;
+
+namespace MissionService.API.Application.Commands;
+
+///
+/// EN: Handler for StartMissionTaskCommand
+/// VI: Handler cho StartMissionTaskCommand
+///
+public class StartMissionTaskCommandHandler : IRequestHandler
+{
+ private readonly IMissionRepository _missionRepository;
+ private readonly IUserTaskRepository _taskRepository;
+ private readonly ILogger _logger;
+
+ public StartMissionTaskCommandHandler(
+ IMissionRepository missionRepository,
+ IUserTaskRepository taskRepository,
+ ILogger logger)
+ {
+ _missionRepository = missionRepository;
+ _taskRepository = taskRepository;
+ _logger = logger;
+ }
+
+ public async Task Handle(StartMissionTaskCommand request, CancellationToken cancellationToken)
+ {
+ // EN: Check if mission exists and is available / VI: Kiểm tra mission tồn tại và khả dụng
+ var mission = await _missionRepository.GetByIdAsync(request.MissionId, cancellationToken);
+ if (mission == null)
+ {
+ return new StartTaskResult(false, null, "Mission not found / Không tìm thấy mission");
+ }
+
+ if (!mission.IsAvailable())
+ {
+ return new StartTaskResult(false, null, "Mission not available / Mission không khả dụng");
+ }
+
+ // EN: Check if user already has active task for this mission
+ // VI: Kiểm tra user đã có task đang hoạt động cho mission này chưa
+ var existingTask = await _taskRepository.GetByUserAndMissionAsync(
+ request.UserId, request.MissionId, cancellationToken);
+
+ if (existingTask != null)
+ {
+ return new StartTaskResult(false, existingTask.Id,
+ "Task already exists / Task đã tồn tại");
+ }
+
+ // EN: Check max completions / VI: Kiểm tra số lần hoàn thành tối đa
+ var completions = await _taskRepository.CountCompletionsByUserAndMissionAsync(
+ request.UserId, request.MissionId, cancellationToken);
+
+ if (completions >= mission.MaxCompletions)
+ {
+ return new StartTaskResult(false, null,
+ "Max completions reached / Đã đạt số lần hoàn thành tối đa");
+ }
+
+ // EN: Create new task / VI: Tạo task mới
+ var task = new UserTask(request.UserId, request.MissionId);
+ _taskRepository.Add(task);
+ await _taskRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
+
+ _logger.LogInformation(
+ "Task {TaskId} started for user {UserId} on mission {MissionId}",
+ task.Id, request.UserId, request.MissionId);
+
+ return new StartTaskResult(true, task.Id, "Task started / Đã bắt đầu task");
+ }
+}
+
+///
+/// EN: Handler for UpdateTaskProgressCommand
+/// VI: Handler cho UpdateTaskProgressCommand
+///
+public class UpdateTaskProgressCommandHandler : IRequestHandler
+{
+ private readonly IUserTaskRepository _taskRepository;
+ private readonly ILogger _logger;
+
+ public UpdateTaskProgressCommandHandler(
+ IUserTaskRepository taskRepository,
+ ILogger logger)
+ {
+ _taskRepository = taskRepository;
+ _logger = logger;
+ }
+
+ public async Task Handle(UpdateTaskProgressCommand request, CancellationToken cancellationToken)
+ {
+ var task = await _taskRepository.GetByIdAsync(request.TaskId, cancellationToken);
+
+ if (task == null)
+ {
+ return new UpdateProgressResult(false, 0, 0, 0, false, "Task not found / Không tìm thấy task");
+ }
+
+ if (task.UserId != request.UserId)
+ {
+ return new UpdateProgressResult(false, 0, 0, 0, false, "Unauthorized / Không có quyền");
+ }
+
+ if (task.Status != TaskStatus.InProgress)
+ {
+ return new UpdateProgressResult(false,
+ task.Progress.CurrentValue,
+ task.Progress.TargetValue,
+ task.Progress.PercentComplete,
+ task.Progress.IsComplete,
+ "Task not in progress / Task không đang thực hiện");
+ }
+
+ try
+ {
+ task.UpdateProgress(request.CurrentValue);
+
+ // EN: Submit evidence if provided / VI: Nộp bằng chứng nếu có
+ if (request.Evidence != null && task.Progress.IsComplete)
+ {
+ var evidence = MapEvidence(request.Evidence);
+ task.SubmitEvidence(evidence);
+ }
+
+ // EN: Auto-complete if no verification needed / VI: Tự động hoàn thành nếu không cần xác thực
+ if (task.Progress.IsComplete && request.Evidence == null)
+ {
+ task.AutoComplete();
+ }
+
+ _taskRepository.Update(task);
+ await _taskRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
+
+ return new UpdateProgressResult(
+ true,
+ task.Progress.CurrentValue,
+ task.Progress.TargetValue,
+ task.Progress.PercentComplete,
+ task.Progress.IsComplete,
+ task.Progress.IsComplete ? "Task completed / Hoàn thành task" : "Progress updated / Đã cập nhật tiến độ");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to update task {TaskId}", request.TaskId);
+ return new UpdateProgressResult(false, 0, 0, 0, false, "Update failed / Cập nhật thất bại");
+ }
+ }
+
+ private static TaskEvidence MapEvidence(TaskEvidenceDto dto)
+ {
+ return dto.Type.ToLowerInvariant() switch
+ {
+ "watchduration" => TaskEvidence.WatchDuration(int.Parse(dto.Data)),
+ "click" => TaskEvidence.Click(dto.Data),
+ "socialproof" => TaskEvidence.SocialProof(dto.ScreenshotUrl ?? "", dto.Data),
+ "invitecode" => TaskEvidence.InviteCode(dto.Data),
+ _ => TaskEvidence.Upload(dto.Data, dto.ScreenshotUrl != null)
+ };
+ }
+}
+
+///
+/// EN: Handler for ClaimTaskRewardCommand
+/// VI: Handler cho ClaimTaskRewardCommand
+///
+public class ClaimTaskRewardCommandHandler : IRequestHandler
+{
+ private readonly IUserTaskRepository _taskRepository;
+ private readonly IMissionRepository _missionRepository;
+ private readonly ILogger _logger;
+
+ public ClaimTaskRewardCommandHandler(
+ IUserTaskRepository taskRepository,
+ IMissionRepository missionRepository,
+ ILogger logger)
+ {
+ _taskRepository = taskRepository;
+ _missionRepository = missionRepository;
+ _logger = logger;
+ }
+
+ public async Task Handle(ClaimTaskRewardCommand request, CancellationToken cancellationToken)
+ {
+ var task = await _taskRepository.GetByIdAsync(request.TaskId, cancellationToken);
+
+ if (task == null)
+ {
+ return new ClaimRewardResult(false, null, "Task not found / Không tìm thấy task");
+ }
+
+ if (task.UserId != request.UserId)
+ {
+ return new ClaimRewardResult(false, null, "Unauthorized / Không có quyền");
+ }
+
+ if (task.Status != TaskStatus.Completed)
+ {
+ return new ClaimRewardResult(false, null, "Task not completed / Task chưa hoàn thành");
+ }
+
+ if (task.RewardClaimed)
+ {
+ return new ClaimRewardResult(false, null, "Reward already claimed / Đã nhận thưởng rồi");
+ }
+
+ try
+ {
+ var mission = await _missionRepository.GetByIdAsync(task.MissionId, cancellationToken);
+ var points = mission?.Reward.Points ?? 0;
+
+ task.ClaimReward();
+ _taskRepository.Update(task);
+ await _taskRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
+
+ _logger.LogInformation(
+ "Reward claimed for task {TaskId} by user {UserId}, points: {Points}",
+ request.TaskId, request.UserId, points);
+
+ return new ClaimRewardResult(true, points, "Reward claimed / Đã nhận thưởng");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to claim reward for task {TaskId}", request.TaskId);
+ return new ClaimRewardResult(false, null, "Claim failed / Nhận thưởng thất bại");
+ }
+ }
+}
diff --git a/services/mission-service-net/src/MissionService.API/Application/Commands/TaskCommands.cs b/services/mission-service-net/src/MissionService.API/Application/Commands/TaskCommands.cs
new file mode 100644
index 00000000..7422c131
--- /dev/null
+++ b/services/mission-service-net/src/MissionService.API/Application/Commands/TaskCommands.cs
@@ -0,0 +1,65 @@
+using MediatR;
+
+namespace MissionService.API.Application.Commands;
+
+///
+/// EN: Command to start a mission task
+/// VI: Command bắt đầu một task mission
+///
+public record StartMissionTaskCommand(Guid UserId, Guid MissionId) : IRequest;
+
+///
+/// EN: Result of starting a task
+/// VI: Kết quả bắt đầu task
+///
+public record StartTaskResult(
+ bool Success,
+ Guid? TaskId,
+ string? Message);
+
+///
+/// EN: Command to update task progress
+/// VI: Command cập nhật tiến độ task
+///
+public record UpdateTaskProgressCommand(
+ Guid UserId,
+ Guid TaskId,
+ int CurrentValue,
+ TaskEvidenceDto? Evidence = null) : IRequest;
+
+///
+/// EN: Task evidence DTO
+/// VI: DTO bằng chứng task
+///
+public record TaskEvidenceDto(
+ string Type,
+ string Data,
+ string? ScreenshotUrl = null,
+ string? VideoUrl = null);
+
+///
+/// EN: Result of updating progress
+/// VI: Kết quả cập nhật tiến độ
+///
+public record UpdateProgressResult(
+ bool Success,
+ int CurrentValue,
+ int TargetValue,
+ decimal PercentComplete,
+ bool IsComplete,
+ string? Message);
+
+///
+/// EN: Command to claim task reward
+/// VI: Command nhận thưởng task
+///
+public record ClaimTaskRewardCommand(Guid UserId, Guid TaskId) : IRequest;
+
+///
+/// EN: Result of claiming reward
+/// VI: Kết quả nhận thưởng
+///
+public record ClaimRewardResult(
+ bool Success,
+ decimal? PointsEarned,
+ string? Message);
diff --git a/services/mission-service-net/src/MissionService.API/Application/Queries/MissionQueries.cs b/services/mission-service-net/src/MissionService.API/Application/Queries/MissionQueries.cs
index 1154aa72..ced7ae3d 100644
--- a/services/mission-service-net/src/MissionService.API/Application/Queries/MissionQueries.cs
+++ b/services/mission-service-net/src/MissionService.API/Application/Queries/MissionQueries.cs
@@ -110,7 +110,7 @@ public record UserTaskProgressDto(
///
public record UserMissionProgressResult(
int TotalMissions,
- int CompletedMissions,
+ int CompletedMissionsCount,
int InProgressMissions,
decimal TotalPointsEarned,
IReadOnlyList ActiveMissions,
diff --git a/services/mission-service-net/src/MissionService.API/Application/Queries/MissionQueryHandlers.cs b/services/mission-service-net/src/MissionService.API/Application/Queries/MissionQueryHandlers.cs
new file mode 100644
index 00000000..8c7e5770
--- /dev/null
+++ b/services/mission-service-net/src/MissionService.API/Application/Queries/MissionQueryHandlers.cs
@@ -0,0 +1,165 @@
+using MediatR;
+using MissionService.Domain.AggregatesModel.MissionAggregate;
+using MissionService.Domain.AggregatesModel.TaskAggregate;
+using TaskStatus = MissionService.Domain.AggregatesModel.TaskAggregate.TaskStatus;
+
+namespace MissionService.API.Application.Queries;
+
+///
+/// EN: Handler for GetAvailableMissionsQuery
+/// VI: Handler cho GetAvailableMissionsQuery
+///
+public class GetAvailableMissionsQueryHandler : IRequestHandler
+{
+ private readonly IMissionRepository _missionRepository;
+ private readonly IUserTaskRepository _taskRepository;
+
+ public GetAvailableMissionsQueryHandler(
+ IMissionRepository missionRepository,
+ IUserTaskRepository taskRepository)
+ {
+ _missionRepository = missionRepository;
+ _taskRepository = taskRepository;
+ }
+
+ public async Task Handle(GetAvailableMissionsQuery request, CancellationToken cancellationToken)
+ {
+ var missions = await _missionRepository.GetActiveMissionsAsync(cancellationToken);
+ var userTasks = await _taskRepository.GetByUserIdAsync(request.UserId, cancellationToken);
+
+ var summaries = missions.Select(m => MapToSummary(m, userTasks)).ToList();
+ return new MissionsListResult(summaries);
+ }
+
+ private static MissionSummaryDto MapToSummary(Mission mission, IReadOnlyList userTasks)
+ {
+ var userTask = userTasks.FirstOrDefault(t => t.MissionId == mission.Id &&
+ (t.Status == TaskStatus.InProgress || t.Status == TaskStatus.PendingVerification));
+
+ return new MissionSummaryDto(
+ Id: mission.Id,
+ Code: mission.Code,
+ Title: mission.TitleEn,
+ Description: mission.DescriptionEn,
+ Type: mission.Type.Name,
+ Category: mission.Category.Name,
+ RewardPoints: mission.Reward.Points,
+ Frequency: mission.Frequency.Name,
+ MaxCompletions: mission.MaxCompletions,
+ IsAvailable: mission.IsAvailable(),
+ UserProgress: userTask != null ? MapToProgress(userTask) : null);
+ }
+
+ private static UserTaskProgressDto MapToProgress(UserTask task)
+ {
+ return new UserTaskProgressDto(
+ TaskId: task.Id,
+ CurrentValue: task.Progress.CurrentValue,
+ TargetValue: task.Progress.TargetValue,
+ PercentComplete: task.Progress.PercentComplete,
+ Status: task.Status.Name,
+ StartedAt: task.StartedAt,
+ CompletedAt: task.CompletedAt,
+ RewardClaimed: task.RewardClaimed);
+ }
+}
+
+///
+/// EN: Handler for GetMissionDetailsQuery
+/// VI: Handler cho GetMissionDetailsQuery
+///
+public class GetMissionDetailsQueryHandler : IRequestHandler
+{
+ private readonly IMissionRepository _missionRepository;
+ private readonly IUserTaskRepository _taskRepository;
+
+ public GetMissionDetailsQueryHandler(
+ IMissionRepository missionRepository,
+ IUserTaskRepository taskRepository)
+ {
+ _missionRepository = missionRepository;
+ _taskRepository = taskRepository;
+ }
+
+ public async Task Handle(GetMissionDetailsQuery request, CancellationToken cancellationToken)
+ {
+ var mission = await _missionRepository.GetByIdAsync(request.MissionId, cancellationToken);
+ if (mission == null) return null;
+
+ UserTaskProgressDto? userProgress = null;
+ if (request.UserId.HasValue)
+ {
+ var userTask = await _taskRepository.GetByUserAndMissionAsync(
+ request.UserId.Value, request.MissionId, cancellationToken);
+ if (userTask != null)
+ {
+ userProgress = new UserTaskProgressDto(
+ TaskId: userTask.Id,
+ CurrentValue: userTask.Progress.CurrentValue,
+ TargetValue: userTask.Progress.TargetValue,
+ PercentComplete: userTask.Progress.PercentComplete,
+ Status: userTask.Status.Name,
+ StartedAt: userTask.StartedAt,
+ CompletedAt: userTask.CompletedAt,
+ RewardClaimed: userTask.RewardClaimed);
+ }
+ }
+
+ return new MissionDetailsResult(
+ Id: mission.Id,
+ Code: mission.Code,
+ TitleEn: mission.TitleEn,
+ TitleVi: mission.TitleVi,
+ DescriptionEn: mission.DescriptionEn,
+ DescriptionVi: mission.DescriptionVi,
+ Type: mission.Type.Name,
+ Category: mission.Category.Name,
+ Reward: new MissionRewardDto(
+ mission.Reward.Points,
+ mission.Reward.MiningBoostPercent,
+ mission.Reward.ExperiencePoints,
+ mission.Reward.BadgeId),
+ Frequency: mission.Frequency.Name,
+ MaxCompletions: mission.MaxCompletions,
+ StartDate: mission.StartDate,
+ EndDate: mission.EndDate,
+ Status: mission.Status.Name,
+ Rules: mission.Rules.Select(r => new MissionRuleDto(r.RuleType, r.Operator, r.Value)).ToList(),
+ UserProgress: userProgress);
+ }
+}
+
+///
+/// EN: Handler for GetMissionsByCategoryQuery
+/// VI: Handler cho GetMissionsByCategoryQuery
+///
+public class GetMissionsByCategoryQueryHandler : IRequestHandler
+{
+ private readonly IMissionRepository _missionRepository;
+
+ public GetMissionsByCategoryQueryHandler(IMissionRepository missionRepository)
+ {
+ _missionRepository = missionRepository;
+ }
+
+ public async Task Handle(GetMissionsByCategoryQuery request, CancellationToken cancellationToken)
+ {
+ var category = MissionCategory.FromDisplayName(request.Category);
+ var missions = await _missionRepository.GetByCategoryAsync(category, cancellationToken);
+
+ var summaries = missions.Select(m => new MissionSummaryDto(
+ Id: m.Id,
+ Code: m.Code,
+ Title: m.TitleEn,
+ Description: m.DescriptionEn,
+ Type: m.Type.Name,
+ Category: m.Category.Name,
+ RewardPoints: m.Reward.Points,
+ Frequency: m.Frequency.Name,
+ MaxCompletions: m.MaxCompletions,
+ IsAvailable: m.IsAvailable(),
+ UserProgress: null)).ToList();
+
+ return new MissionsListResult(summaries);
+ }
+}
diff --git a/services/mission-service-net/src/MissionService.API/Controllers/AdminController.cs b/services/mission-service-net/src/MissionService.API/Controllers/AdminController.cs
new file mode 100644
index 00000000..82612cbf
--- /dev/null
+++ b/services/mission-service-net/src/MissionService.API/Controllers/AdminController.cs
@@ -0,0 +1,241 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using MissionService.Domain.AggregatesModel.CheckInAggregate;
+using MissionService.Domain.AggregatesModel.TaskAggregate;
+using TaskStatus = MissionService.Domain.AggregatesModel.TaskAggregate.TaskStatus;
+
+namespace MissionService.API.Controllers;
+
+///
+/// EN: Admin controller for check-in and task management
+/// VI: Controller admin để quản lý check-in và task
+///
+[ApiController]
+[Route("api/v1/admin")]
+[Authorize(Roles = "Admin")]
+public class AdminController : ControllerBase
+{
+ private readonly IUserCheckInRepository _checkInRepository;
+ private readonly IUserTaskRepository _taskRepository;
+ private readonly ILogger _logger;
+
+ public AdminController(
+ IUserCheckInRepository checkInRepository,
+ IUserTaskRepository taskRepository,
+ ILogger logger)
+ {
+ _checkInRepository = checkInRepository;
+ _taskRepository = taskRepository;
+ _logger = logger;
+ }
+
+ #region Check-In Admin
+
+ ///
+ /// EN: Get user's check-in details / VI: Lấy chi tiết check-in của user
+ ///
+ [HttpGet("checkins/users/{userId:guid}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task GetUserCheckIn(Guid userId)
+ {
+ var checkIn = await _checkInRepository.GetByUserIdAsync(userId);
+ if (checkIn == null)
+ {
+ return NotFound(new { Message = "Check-in profile not found" });
+ }
+
+ return Ok(new
+ {
+ checkIn.Id,
+ checkIn.UserId,
+ checkIn.CurrentStreak,
+ checkIn.LongestStreak,
+ checkIn.TotalCheckIns,
+ checkIn.LastCheckInDate,
+ CanCheckInToday = checkIn.CanCheckInToday()
+ });
+ }
+
+ ///
+ /// EN: Reset user's streak (admin action) / VI: Reset streak của user (hành động admin)
+ ///
+ [HttpPost("checkins/users/{userId:guid}/reset-streak")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task ResetUserStreak(Guid userId)
+ {
+ var checkIn = await _checkInRepository.GetByUserIdAsync(userId);
+ if (checkIn == null)
+ {
+ return NotFound(new { Message = "Check-in profile not found" });
+ }
+
+ checkIn.ResetStreak();
+ _checkInRepository.Update(checkIn);
+ await _checkInRepository.UnitOfWork.SaveEntitiesAsync();
+
+ _logger.LogWarning("Admin reset streak for user {UserId}", userId);
+
+ return Ok(new { Message = "Streak reset successfully", checkIn.CurrentStreak });
+ }
+
+ ///
+ /// EN: Get top streaks / VI: Lấy streaks cao nhất
+ ///
+ [HttpGet("checkins/top-streaks")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task GetTopStreaks([FromQuery] int count = 20)
+ {
+ var topStreaks = await _checkInRepository.GetTopStreaksAsync(Math.Min(count, 100));
+
+ return Ok(topStreaks.Select((c, i) => new
+ {
+ Rank = i + 1,
+ c.UserId,
+ c.CurrentStreak,
+ c.LongestStreak,
+ c.TotalCheckIns,
+ c.LastCheckInDate
+ }));
+ }
+
+ #endregion
+
+ #region Task Admin
+
+ ///
+ /// EN: Get pending verification tasks / VI: Lấy tasks đang chờ xác thực
+ ///
+ [HttpGet("tasks/pending-verification")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task GetPendingVerificationTasks()
+ {
+ var tasks = await _taskRepository.GetPendingVerificationAsync();
+
+ return Ok(tasks.Select(t => new
+ {
+ t.Id,
+ t.UserId,
+ t.MissionId,
+ Status = t.Status.Name,
+ Progress = new
+ {
+ t.Progress.CurrentValue,
+ t.Progress.TargetValue,
+ t.Progress.PercentComplete
+ },
+ Evidence = t.Evidence != null ? new
+ {
+ Type = t.Evidence.Type.ToString(),
+ t.Evidence.Data,
+ t.Evidence.ScreenshotUrl,
+ t.Evidence.VideoUrl,
+ t.Evidence.CapturedAt
+ } : null,
+ t.StartedAt
+ }));
+ }
+
+ ///
+ /// EN: Approve a task / VI: Duyệt task
+ ///
+ [HttpPost("tasks/{taskId:guid}/approve")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task ApproveTask(Guid taskId)
+ {
+ var task = await _taskRepository.GetByIdAsync(taskId);
+ if (task == null)
+ {
+ return NotFound(new { Message = "Task not found" });
+ }
+
+ if (task.Status != TaskStatus.PendingVerification)
+ {
+ return BadRequest(new { Message = "Task is not pending verification" });
+ }
+
+ var adminId = GetCurrentUserId();
+ task.Complete(VerificationResult.ManualApproved(adminId));
+ _taskRepository.Update(task);
+ await _taskRepository.UnitOfWork.SaveEntitiesAsync();
+
+ _logger.LogInformation("Task {TaskId} approved by admin {AdminId}", taskId, adminId);
+
+ return Ok(new { Message = "Task approved", Status = task.Status.Name });
+ }
+
+ ///
+ /// EN: Reject a task / VI: Từ chối task
+ ///
+ [HttpPost("tasks/{taskId:guid}/reject")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task RejectTask(Guid taskId, [FromBody] RejectTaskRequest request)
+ {
+ var task = await _taskRepository.GetByIdAsync(taskId);
+ if (task == null)
+ {
+ return NotFound(new { Message = "Task not found" });
+ }
+
+ if (task.Status != TaskStatus.PendingVerification)
+ {
+ return BadRequest(new { Message = "Task is not pending verification" });
+ }
+
+ var adminId = GetCurrentUserId();
+ task.Complete(VerificationResult.Rejected(request.Reason, VerificationMethod.Manual, adminId));
+ _taskRepository.Update(task);
+ await _taskRepository.UnitOfWork.SaveEntitiesAsync();
+
+ _logger.LogInformation("Task {TaskId} rejected by admin {AdminId}: {Reason}", taskId, adminId, request.Reason);
+
+ return Ok(new { Message = "Task rejected", Status = task.Status.Name, request.Reason });
+ }
+
+ ///
+ /// EN: Get user's tasks / VI: Lấy tasks của user
+ ///
+ [HttpGet("tasks/users/{userId:guid}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task GetUserTasks(Guid userId)
+ {
+ var tasks = await _taskRepository.GetByUserIdAsync(userId);
+
+ return Ok(tasks.Select(t => new
+ {
+ t.Id,
+ t.MissionId,
+ Status = t.Status.Name,
+ Progress = new
+ {
+ t.Progress.CurrentValue,
+ t.Progress.TargetValue,
+ t.Progress.PercentComplete
+ },
+ t.RewardClaimed,
+ t.StartedAt,
+ t.CompletedAt,
+ t.ClaimedAt
+ }));
+ }
+
+ #endregion
+
+ private Guid GetCurrentUserId()
+ {
+ var userIdClaim = User.FindFirst("sub") ?? User.FindFirst("user_id");
+ if (userIdClaim != null && Guid.TryParse(userIdClaim.Value, out var userId))
+ {
+ return userId;
+ }
+ throw new UnauthorizedAccessException("User ID not found in token");
+ }
+}
+
+///
+/// EN: Request to reject a task / VI: Request từ chối task
+///
+public record RejectTaskRequest(string Reason);
diff --git a/services/mission-service-net/src/MissionService.API/Controllers/AdminMissionsController.cs b/services/mission-service-net/src/MissionService.API/Controllers/AdminMissionsController.cs
new file mode 100644
index 00000000..97d2c179
--- /dev/null
+++ b/services/mission-service-net/src/MissionService.API/Controllers/AdminMissionsController.cs
@@ -0,0 +1,223 @@
+using MediatR;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using MissionService.Domain.AggregatesModel.MissionAggregate;
+
+namespace MissionService.API.Controllers;
+
+///
+/// EN: Admin controller for mission management
+/// VI: Controller admin để quản lý mission
+///
+[ApiController]
+[Route("api/v1/admin/missions")]
+[Authorize(Roles = "Admin")]
+public class AdminMissionsController : ControllerBase
+{
+ private readonly IMissionRepository _missionRepository;
+ private readonly ILogger _logger;
+
+ public AdminMissionsController(
+ IMissionRepository missionRepository,
+ ILogger logger)
+ {
+ _missionRepository = missionRepository;
+ _logger = logger;
+ }
+
+ ///
+ /// EN: Get all missions (admin) / VI: Lấy tất cả missions (admin)
+ ///
+ [HttpGet]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task GetAllMissions()
+ {
+ var missions = await _missionRepository.GetActiveMissionsAsync();
+ return Ok(missions.Select(m => MapToAdminDto(m)));
+ }
+
+ ///
+ /// EN: Create new mission / VI: Tạo mission mới
+ ///
+ [HttpPost]
+ [ProducesResponseType(StatusCodes.Status201Created)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task CreateMission([FromBody] CreateMissionRequest request)
+ {
+ try
+ {
+ var reward = MissionReward.Create(
+ request.RewardPoints,
+ request.RewardMiningBoost,
+ request.RewardXp,
+ request.RewardBadgeId);
+
+ var mission = new Mission(
+ code: request.Code,
+ titleEn: request.TitleEn,
+ titleVi: request.TitleVi,
+ type: MissionType.FromValue(request.TypeId),
+ category: MissionCategory.FromValue(request.CategoryId),
+ reward: reward,
+ frequency: FrequencyType.FromValue(request.FrequencyId),
+ maxCompletions: request.MaxCompletions,
+ startDate: request.StartDate,
+ endDate: request.EndDate,
+ priority: request.Priority);
+
+ _missionRepository.Add(mission);
+ await _missionRepository.UnitOfWork.SaveEntitiesAsync();
+
+ _logger.LogInformation("Mission {Code} created by admin", request.Code);
+
+ return CreatedAtAction(nameof(GetMissionById), new { id = mission.Id }, MapToAdminDto(mission));
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Failed to create mission {Code}", request.Code);
+ return BadRequest(new { Message = ex.Message });
+ }
+ }
+
+ ///
+ /// EN: Get mission by ID / VI: Lấy mission theo ID
+ ///
+ [HttpGet("{id:guid}")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task GetMissionById(Guid id)
+ {
+ var mission = await _missionRepository.GetByIdAsync(id);
+ if (mission == null)
+ {
+ return NotFound();
+ }
+ return Ok(MapToAdminDto(mission));
+ }
+
+ ///
+ /// EN: Activate a mission / VI: Kích hoạt mission
+ ///
+ [HttpPost("{id:guid}/activate")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task ActivateMission(Guid id)
+ {
+ var mission = await _missionRepository.GetByIdAsync(id);
+ if (mission == null)
+ {
+ return NotFound();
+ }
+
+ try
+ {
+ mission.Activate();
+ _missionRepository.Update(mission);
+ await _missionRepository.UnitOfWork.SaveEntitiesAsync();
+
+ _logger.LogInformation("Mission {Id} activated", id);
+ return Ok(MapToAdminDto(mission));
+ }
+ catch (InvalidOperationException ex)
+ {
+ return BadRequest(new { Message = ex.Message });
+ }
+ }
+
+ ///
+ /// EN: Pause a mission / VI: Tạm dừng mission
+ ///
+ [HttpPost("{id:guid}/pause")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task PauseMission(Guid id)
+ {
+ var mission = await _missionRepository.GetByIdAsync(id);
+ if (mission == null)
+ {
+ return NotFound();
+ }
+
+ try
+ {
+ mission.Pause();
+ _missionRepository.Update(mission);
+ await _missionRepository.UnitOfWork.SaveEntitiesAsync();
+
+ _logger.LogInformation("Mission {Id} paused", id);
+ return Ok(MapToAdminDto(mission));
+ }
+ catch (InvalidOperationException ex)
+ {
+ return BadRequest(new { Message = ex.Message });
+ }
+ }
+
+ ///
+ /// EN: Archive a mission / VI: Lưu trữ mission
+ ///
+ [HttpPost("{id:guid}/archive")]
+ [ProducesResponseType(StatusCodes.Status200OK)]
+ public async Task ArchiveMission(Guid id)
+ {
+ var mission = await _missionRepository.GetByIdAsync(id);
+ if (mission == null)
+ {
+ return NotFound();
+ }
+
+ mission.Archive();
+ _missionRepository.Update(mission);
+ await _missionRepository.UnitOfWork.SaveEntitiesAsync();
+
+ _logger.LogInformation("Mission {Id} archived", id);
+ return Ok(MapToAdminDto(mission));
+ }
+
+ private static object MapToAdminDto(Mission m) => new
+ {
+ m.Id,
+ m.Code,
+ m.TitleEn,
+ m.TitleVi,
+ m.DescriptionEn,
+ m.DescriptionVi,
+ Type = m.Type.Name,
+ Category = m.Category.Name,
+ Status = m.Status.Name,
+ Frequency = m.Frequency.Name,
+ Reward = new
+ {
+ m.Reward.Points,
+ m.Reward.MiningBoostPercent,
+ m.Reward.ExperiencePoints,
+ m.Reward.BadgeId
+ },
+ m.MaxCompletions,
+ m.StartDate,
+ m.EndDate,
+ m.Priority,
+ Rules = m.Rules.Select(r => new { r.RuleType, r.Operator, r.Value })
+ };
+}
+
+///
+/// EN: Request to create a new mission / VI: Request tạo mission mới
+///
+public record CreateMissionRequest(
+ string Code,
+ string TitleEn,
+ string TitleVi,
+ string? DescriptionEn,
+ string? DescriptionVi,
+ int TypeId,
+ int CategoryId,
+ int FrequencyId,
+ decimal RewardPoints,
+ decimal RewardMiningBoost,
+ int RewardXp,
+ string? RewardBadgeId,
+ int MaxCompletions,
+ DateTime StartDate,
+ DateTime? EndDate,
+ int Priority = 0);
diff --git a/services/mission-service-net/src/MissionService.API/Controllers/MissionsController.cs b/services/mission-service-net/src/MissionService.API/Controllers/MissionsController.cs
new file mode 100644
index 00000000..337e13b6
--- /dev/null
+++ b/services/mission-service-net/src/MissionService.API/Controllers/MissionsController.cs
@@ -0,0 +1,156 @@
+using MediatR;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+using MissionService.API.Application.Commands;
+using MissionService.API.Application.Queries;
+
+namespace MissionService.API.Controllers;
+
+///
+/// EN: Controller for mission operations
+/// VI: Controller cho các thao tác mission
+///
+[ApiController]
+[Route("api/v1/[controller]")]
+[Authorize]
+public class MissionsController : ControllerBase
+{
+ private readonly IMediator _mediator;
+ private readonly ILogger _logger;
+
+ public MissionsController(IMediator mediator, ILogger logger)
+ {
+ _mediator = mediator;
+ _logger = logger;
+ }
+
+ ///
+ /// EN: Get all available missions / VI: Lấy tất cả missions khả dụng
+ ///
+ [HttpGet]
+ [ProducesResponseType(typeof(MissionsListResult), StatusCodes.Status200OK)]
+ public async Task GetAvailableMissions()
+ {
+ var userId = GetCurrentUserId();
+ var result = await _mediator.Send(new GetAvailableMissionsQuery(userId));
+ return Ok(result);
+ }
+
+ ///
+ /// EN: Get missions by category / VI: Lấy missions theo danh mục
+ ///
+ [HttpGet("category/{category}")]
+ [AllowAnonymous]
+ [ProducesResponseType(typeof(MissionsListResult), StatusCodes.Status200OK)]
+ public async Task GetByCategory(string category)
+ {
+ var result = await _mediator.Send(new GetMissionsByCategoryQuery(category));
+ return Ok(result);
+ }
+
+ ///
+ /// EN: Get mission details / VI: Lấy chi tiết mission
+ ///
+ [HttpGet("{id:guid}")]
+ [AllowAnonymous]
+ [ProducesResponseType(typeof(MissionDetailsResult), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status404NotFound)]
+ public async Task GetDetails(Guid id)
+ {
+ var userId = TryGetCurrentUserId();
+ var result = await _mediator.Send(new GetMissionDetailsQuery(id, userId));
+
+ if (result == null)
+ {
+ return NotFound(new { Message = "Mission not found / Không tìm thấy mission" });
+ }
+
+ return Ok(result);
+ }
+
+ ///
+ /// EN: Start a mission task / VI: Bắt đầu task mission
+ ///
+ [HttpPost("{id:guid}/start")]
+ [ProducesResponseType(typeof(StartTaskResult), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task StartMission(Guid id)
+ {
+ var userId = GetCurrentUserId();
+ var result = await _mediator.Send(new StartMissionTaskCommand(userId, id));
+
+ if (!result.Success)
+ {
+ return BadRequest(result);
+ }
+
+ return Ok(result);
+ }
+
+ ///
+ /// EN: Update task progress / VI: Cập nhật tiến độ task
+ ///
+ [HttpPut("tasks/{taskId:guid}/progress")]
+ [ProducesResponseType(typeof(UpdateProgressResult), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task UpdateProgress(Guid taskId, [FromBody] UpdateProgressRequest request)
+ {
+ var userId = GetCurrentUserId();
+ var result = await _mediator.Send(new UpdateTaskProgressCommand(
+ userId,
+ taskId,
+ request.CurrentValue,
+ request.Evidence));
+
+ if (!result.Success)
+ {
+ return BadRequest(result);
+ }
+
+ return Ok(result);
+ }
+
+ ///
+ /// EN: Claim task reward / VI: Nhận thưởng task
+ ///
+ [HttpPost("tasks/{taskId:guid}/claim")]
+ [ProducesResponseType(typeof(ClaimRewardResult), StatusCodes.Status200OK)]
+ [ProducesResponseType(StatusCodes.Status400BadRequest)]
+ public async Task ClaimReward(Guid taskId)
+ {
+ var userId = GetCurrentUserId();
+ var result = await _mediator.Send(new ClaimTaskRewardCommand(userId, taskId));
+
+ if (!result.Success)
+ {
+ return BadRequest(result);
+ }
+
+ return Ok(result);
+ }
+
+ private Guid GetCurrentUserId()
+ {
+ var userIdClaim = User.FindFirst("sub") ?? User.FindFirst("user_id");
+ if (userIdClaim != null && Guid.TryParse(userIdClaim.Value, out var userId))
+ {
+ return userId;
+ }
+ throw new UnauthorizedAccessException("User ID not found in token");
+ }
+
+ private Guid? TryGetCurrentUserId()
+ {
+ var userIdClaim = User.FindFirst("sub") ?? User.FindFirst("user_id");
+ if (userIdClaim != null && Guid.TryParse(userIdClaim.Value, out var userId))
+ {
+ return userId;
+ }
+ return null;
+ }
+}
+
+///
+/// EN: Request to update progress / VI: Request cập nhật tiến độ
+///
+public record UpdateProgressRequest(int CurrentValue, TaskEvidenceDto? Evidence = null);