diff --git a/deployments/local/docker-compose.yml b/deployments/local/docker-compose.yml
index ac5620af..938fbb8a 100644
--- a/deployments/local/docker-compose.yml
+++ b/deployments/local/docker-compose.yml
@@ -478,6 +478,57 @@ services:
- "traefik.http.services.mining-service.loadbalancer.sticky.cookie=true"
- "traefik.http.services.mining-service.loadbalancer.sticky.cookie.name=mining_session"
+ # Mission Service .NET - Gamification (Check-ins, Missions, Tasks)
+ mission-service-net:
+ build:
+ context: ../../services/mission-service-net
+ dockerfile: Dockerfile
+ image: goodgo/mission-service-net:latest
+ container_name: mission-service-net-local
+ environment:
+ - ASPNETCORE_ENVIRONMENT=Development
+ - ASPNETCORE_URLS=http://+:8080
+ # EN: Database - Neon PostgreSQL
+ # VI: Cơ sở dữ liệu - Neon PostgreSQL
+ - ConnectionStrings__DefaultConnection=Host=ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech;Port=5432;Database=mission_service;Username=neondb_owner;Password=npg_Ssfy6HKO0cXI;SSL Mode=Require
+ # EN: IAM Service Communication
+ # VI: Giao tiếp IAM Service
+ - IamService__BaseUrl=http://iam-service-net:8080
+ - IamService__ServiceName=mission-service
+ # EN: JWT Configuration
+ # VI: Cấu hình JWT
+ - Jwt__Authority=http://iam-service-net:8080
+ - Jwt__Audience=goodgo-api
+ - Jwt__RequireHttpsMetadata=false
+ # EN: Redis Cache
+ # VI: Cache Redis
+ - Redis__Host=167.114.174.113
+ - Redis__Port=6379
+ - Redis__Password=Velik@2026
+ ports:
+ - "5007:8080"
+ depends_on:
+ iam-service-net:
+ condition: service_healthy
+ traefik:
+ condition: service_started
+ networks:
+ - microservices-network
+ restart: unless-stopped
+ healthcheck:
+ test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 40s
+ labels:
+ - "traefik.enable=true"
+ - "traefik.http.routers.mission-service.rule=PathPrefix(`/api/v1/checkins`) || PathPrefix(`/api/v1/missions`) || PathPrefix(`/api/v1/admin/missions`) || PathPrefix(`/api/v1/admin/checkins`) || PathPrefix(`/api/v1/admin/tasks`)"
+ - "traefik.http.routers.mission-service.entrypoints=web"
+ - "traefik.http.services.mission-service.loadbalancer.server.port=8080"
+ - "traefik.http.services.mission-service.loadbalancer.healthcheck.path=/health/live"
+ - "traefik.http.services.mission-service.loadbalancer.healthcheck.interval=10s"
+
# Jaeger - Distributed Tracing
# jaeger:
diff --git a/services/mining-service-net/docs/en/README.md b/services/mining-service-net/docs/en/README.md
index 5310fcc5..fdabc281 100644
--- a/services/mining-service-net/docs/en/README.md
+++ b/services/mining-service-net/docs/en/README.md
@@ -401,9 +401,11 @@ sequenceDiagram
| Method | Endpoint | Description |
|--------|----------|-------------|
| `GET` | `/api/v1/admin/analytics/overview` | Dashboard overview stats |
-| `GET` | `/api/v1/admin/analytics/mining` | Mining statistics |
-| `GET` | `/api/v1/admin/analytics/streaks` | Streak distribution |
+| `GET` | `/api/v1/admin/analytics/miners` | Miner statistics |
+| `GET` | `/api/v1/admin/analytics/circles` | Circle statistics |
| `GET` | `/api/v1/admin/analytics/referrals` | Referral network stats |
+| `GET` | `/api/v1/admin/analytics/points` | Points statistics |
+| `GET` | `/api/v1/admin/analytics/streaks` | Streak distribution |
| `GET` | `/api/v1/admin/audit-logs` | View configuration change logs |
---
diff --git a/services/mining-service-net/docs/vi/README.md b/services/mining-service-net/docs/vi/README.md
index 45759116..4ab32975 100644
--- a/services/mining-service-net/docs/vi/README.md
+++ b/services/mining-service-net/docs/vi/README.md
@@ -401,9 +401,11 @@ sequenceDiagram
| Phương Thức | Endpoint | Mô Tả |
|-------------|----------|-------|
| `GET` | `/api/v1/admin/analytics/overview` | Thống kê tổng quan dashboard |
-| `GET` | `/api/v1/admin/analytics/mining` | Thống kê đào |
-| `GET` | `/api/v1/admin/analytics/streaks` | Phân bố streak |
+| `GET` | `/api/v1/admin/analytics/miners` | Thống kê thợ đào |
+| `GET` | `/api/v1/admin/analytics/circles` | Thống kê vòng tròn an toàn |
| `GET` | `/api/v1/admin/analytics/referrals` | Thống kê mạng lưới giới thiệu |
+| `GET` | `/api/v1/admin/analytics/points` | Thống kê điểm số |
+| `GET` | `/api/v1/admin/analytics/streaks` | Phân bố streak |
| `GET` | `/api/v1/admin/audit-logs` | Xem nhật ký thay đổi cấu hình |
---
diff --git a/services/mining-service-net/tests/MiningService.UnitTests/Application/Commands/AdminCommandHandlerTests.cs b/services/mining-service-net/tests/MiningService.UnitTests/Application/Commands/AdminCommandHandlerTests.cs
new file mode 100644
index 00000000..4ca8fdb6
--- /dev/null
+++ b/services/mining-service-net/tests/MiningService.UnitTests/Application/Commands/AdminCommandHandlerTests.cs
@@ -0,0 +1,134 @@
+using FluentAssertions;
+using MiningService.API.Application.Commands;
+using MiningService.Domain.AggregatesModel.MinerAggregate;
+using MiningService.Domain.Exceptions;
+using NSubstitute;
+using Xunit;
+
+namespace MiningService.UnitTests.Application.Commands;
+
+///
+/// EN: Unit tests for Admin Command Handlers.
+/// VI: Unit tests cho Admin Command Handlers.
+///
+public class AdminCommandHandlerTests
+{
+ private readonly IMinerRepository _minerRepository;
+
+ public AdminCommandHandlerTests()
+ {
+ _minerRepository = Substitute.For();
+ }
+
+ #region BanMinerCommandHandler Tests
+
+ [Fact]
+ public async Task BanMiner_ExistingMiner_BansSuccessfully()
+ {
+ // Arrange
+ var minerId = Guid.NewGuid();
+ var miner = Miner.Create(Guid.NewGuid());
+ var command = new BanMinerCommand(minerId, "Violation");
+ var handler = new BanMinerCommandHandler(_minerRepository);
+
+ _minerRepository.GetByIdAsync(minerId, Arg.Any())
+ .Returns(miner);
+ _minerRepository.UnitOfWork.SaveEntitiesAsync(Arg.Any())
+ .Returns(true);
+
+ // Act
+ var result = await handler.Handle(command, CancellationToken.None);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Success.Should().BeTrue();
+ miner.Status.Should().Be(MinerStatus.Suspended);
+ }
+
+ [Fact]
+ public async Task BanMiner_MinerNotFound_ThrowsException()
+ {
+ // Arrange
+ var minerId = Guid.NewGuid();
+ var command = new BanMinerCommand(minerId, "Violation");
+ var handler = new BanMinerCommandHandler(_minerRepository);
+
+ _minerRepository.GetByIdAsync(minerId, Arg.Any())
+ .Returns((Miner?)null);
+
+ // Act & Assert
+ await Assert.ThrowsAsync(() =>
+ handler.Handle(command, CancellationToken.None));
+ }
+
+ #endregion
+
+ #region AdjustMinerPointsCommandHandler Tests
+
+ [Fact]
+ public async Task AdjustPoints_PositiveAmount_AddsPoints()
+ {
+ // Arrange
+ var minerId = Guid.NewGuid();
+ var miner = Miner.Create(Guid.NewGuid());
+ var command = new AdjustMinerPointsCommand(minerId, 100m, "Admin bonus");
+ var handler = new AdjustMinerPointsCommandHandler(_minerRepository);
+
+ _minerRepository.GetByIdAsync(minerId, Arg.Any())
+ .Returns(miner);
+ _minerRepository.UnitOfWork.SaveEntitiesAsync(Arg.Any())
+ .Returns(true);
+
+ // Act
+ var result = await handler.Handle(command, CancellationToken.None);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Success.Should().BeTrue();
+ result.NewBalance.Should().Be(100m);
+ }
+
+ [Fact]
+ public async Task AdjustPoints_MinerNotFound_ThrowsException()
+ {
+ // Arrange
+ var minerId = Guid.NewGuid();
+ var command = new AdjustMinerPointsCommand(minerId, 100m, "Bonus");
+ var handler = new AdjustMinerPointsCommandHandler(_minerRepository);
+
+ _minerRepository.GetByIdAsync(minerId, Arg.Any())
+ .Returns((Miner?)null);
+
+ // Act & Assert
+ await Assert.ThrowsAsync(() =>
+ handler.Handle(command, CancellationToken.None));
+ }
+
+ #endregion
+
+ #region ResetMinerStreakCommandHandler Tests
+
+ [Fact]
+ public async Task ResetStreak_ValidMiner_ResetsSuccessfully()
+ {
+ // Arrange
+ var minerId = Guid.NewGuid();
+ var miner = Miner.Create(Guid.NewGuid());
+ var command = new ResetMinerStreakCommand(minerId, "Admin reset");
+ var handler = new ResetMinerStreakCommandHandler(_minerRepository);
+
+ _minerRepository.GetByIdAsync(minerId, Arg.Any())
+ .Returns(miner);
+ _minerRepository.UnitOfWork.SaveEntitiesAsync(Arg.Any())
+ .Returns(true);
+
+ // Act
+ var result = await handler.Handle(command, CancellationToken.None);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.Success.Should().BeTrue();
+ }
+
+ #endregion
+}
diff --git a/services/mining-service-net/tests/MiningService.UnitTests/Application/Commands/CircleCommandHandlerTests.cs b/services/mining-service-net/tests/MiningService.UnitTests/Application/Commands/CircleCommandHandlerTests.cs
new file mode 100644
index 00000000..a42c672f
--- /dev/null
+++ b/services/mining-service-net/tests/MiningService.UnitTests/Application/Commands/CircleCommandHandlerTests.cs
@@ -0,0 +1,102 @@
+using FluentAssertions;
+using MiningService.API.Application.Commands;
+using MiningService.Domain.AggregatesModel.CircleAggregate;
+using MiningService.Domain.AggregatesModel.MinerAggregate;
+using NSubstitute;
+using Xunit;
+
+namespace MiningService.UnitTests.Application.Commands;
+
+///
+/// EN: Unit tests for Circle Command Handlers.
+/// VI: Unit tests cho Circle Command Handlers.
+///
+public class CircleCommandHandlerTests
+{
+ private readonly ICircleRepository _circleRepository;
+ private readonly IMinerRepository _minerRepository;
+
+ public CircleCommandHandlerTests()
+ {
+ _circleRepository = Substitute.For();
+ _minerRepository = Substitute.For();
+ }
+
+ #region CreateCircleCommandHandler Tests
+
+ [Fact]
+ public async Task CreateCircle_ValidInput_CreatesNewCircle()
+ {
+ // Arrange
+ var userId = Guid.NewGuid();
+ var miner = Miner.Create(userId);
+ var command = new CreateCircleCommand(userId, "Test Circle");
+ var handler = new CreateCircleCommandHandler(_circleRepository, _minerRepository);
+
+ _minerRepository.GetByUserIdAsync(userId, Arg.Any())
+ .Returns(miner);
+ _circleRepository.UnitOfWork.SaveEntitiesAsync(Arg.Any())
+ .Returns(true);
+
+ // Act
+ var result = await handler.Handle(command, CancellationToken.None);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.CircleId.Should().NotBeEmpty();
+
+ _circleRepository.Received(1).Add(Arg.Is(c => c.Name == "Test Circle"));
+ }
+
+ [Fact]
+ public async Task CreateCircle_MinerAlreadyInCircle_ThrowsException()
+ {
+ // Arrange
+ var userId = Guid.NewGuid();
+ var miner = Miner.Create(userId);
+ miner.JoinCircle(Guid.NewGuid()); // Already in a circle
+ var command = new CreateCircleCommand(userId, "New Circle");
+ var handler = new CreateCircleCommandHandler(_circleRepository, _minerRepository);
+
+ _minerRepository.GetByUserIdAsync(userId, Arg.Any())
+ .Returns(miner);
+
+ // Act & Assert
+ var act = () => handler.Handle(command, CancellationToken.None);
+ await act.Should().ThrowAsync().WithMessage("*already*");
+ }
+
+ #endregion
+
+ #region InviteToCircleCommandHandler Tests
+
+ [Fact]
+ public async Task InviteToCircle_ValidInput_ReturnsTrue()
+ {
+ // Arrange
+ var ownerId = Guid.NewGuid();
+ var inviteeId = Guid.NewGuid();
+ var circle = Circle.Create(ownerId, "Test Circle");
+ var inviter = Miner.Create(ownerId);
+ var invitee = Miner.Create(inviteeId);
+ var command = new InviteToCircleCommand(ownerId, inviteeId);
+ var handler = new InviteToCircleCommandHandler(_circleRepository, _minerRepository);
+
+ _circleRepository.GetByOwnerIdAsync(ownerId, Arg.Any())
+ .Returns(circle);
+ _minerRepository.GetByUserIdAsync(ownerId, Arg.Any())
+ .Returns(inviter);
+ _minerRepository.GetByUserIdAsync(inviteeId, Arg.Any())
+ .Returns(invitee);
+ _circleRepository.UnitOfWork.SaveEntitiesAsync(Arg.Any())
+ .Returns(true);
+
+ // Act
+ var result = await handler.Handle(command, CancellationToken.None);
+
+ // Assert
+ result.Should().BeTrue();
+ }
+
+ #endregion
+}
diff --git a/services/mining-service-net/tests/MiningService.UnitTests/Application/Commands/ClaimMiningRewardCommandHandlerTests.cs b/services/mining-service-net/tests/MiningService.UnitTests/Application/Commands/ClaimMiningRewardCommandHandlerTests.cs
new file mode 100644
index 00000000..10eacc8c
--- /dev/null
+++ b/services/mining-service-net/tests/MiningService.UnitTests/Application/Commands/ClaimMiningRewardCommandHandlerTests.cs
@@ -0,0 +1,84 @@
+using FluentAssertions;
+using MiningService.API.Application.Commands;
+using MiningService.Domain.AggregatesModel.MinerAggregate;
+using MiningService.Domain.Exceptions;
+using MiningService.Domain.SeedWork;
+using NSubstitute;
+using Xunit;
+
+namespace MiningService.UnitTests.Application.Commands;
+
+///
+/// EN: Unit tests for ClaimMiningRewardCommandHandler.
+/// VI: Unit tests cho ClaimMiningRewardCommandHandler.
+///
+public class ClaimMiningRewardCommandHandlerTests
+{
+ private readonly IMinerRepository _minerRepository;
+ private readonly IUnitOfWork _unitOfWork;
+ private readonly ClaimMiningRewardCommandHandler _handler;
+
+ public ClaimMiningRewardCommandHandlerTests()
+ {
+ _unitOfWork = Substitute.For();
+ _minerRepository = Substitute.For();
+ _minerRepository.UnitOfWork.Returns(_unitOfWork);
+ _handler = new ClaimMiningRewardCommandHandler(_minerRepository);
+ }
+
+ [Fact]
+ public async Task Handle_ReadySession_ReturnsReward()
+ {
+ // Arrange
+ var userId = Guid.NewGuid();
+ var miner = Miner.Create(userId);
+ miner.StartMiningSession(configBaseRate: 0.25m, sessionHours: 0); // Immediate claim
+ var command = new ClaimMiningRewardCommand(userId);
+
+ _minerRepository.GetByUserIdAsync(userId, Arg.Any())
+ .Returns(miner);
+ _minerRepository.UnitOfWork.SaveEntitiesAsync(Arg.Any())
+ .Returns(true);
+
+ // Act
+ var result = await _handler.Handle(command, CancellationToken.None);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.PointsEarned.Should().BeGreaterThanOrEqualTo(0);
+ result.TotalPoints.Should().BeGreaterThanOrEqualTo(0);
+
+ await _minerRepository.Received(1).UnitOfWork.SaveEntitiesAsync(Arg.Any());
+ }
+
+ [Fact]
+ public async Task Handle_MinerNotFound_ThrowsNotFoundException()
+ {
+ // Arrange
+ var userId = Guid.NewGuid();
+ var command = new ClaimMiningRewardCommand(userId);
+
+ _minerRepository.GetByUserIdAsync(userId, Arg.Any())
+ .Returns((Miner?)null);
+
+ // Act & Assert
+ await Assert.ThrowsAsync(() =>
+ _handler.Handle(command, CancellationToken.None));
+ }
+
+ [Fact]
+ public async Task Handle_NoActiveSession_ThrowsDomainException()
+ {
+ // Arrange
+ var userId = Guid.NewGuid();
+ var miner = Miner.Create(userId); // No session started
+ var command = new ClaimMiningRewardCommand(userId);
+
+ _minerRepository.GetByUserIdAsync(userId, Arg.Any())
+ .Returns(miner);
+
+ // Act & Assert
+ await Assert.ThrowsAsync(() =>
+ _handler.Handle(command, CancellationToken.None));
+ }
+}
diff --git a/services/mining-service-net/tests/MiningService.UnitTests/Application/Commands/StartMiningCommandHandlerTests.cs b/services/mining-service-net/tests/MiningService.UnitTests/Application/Commands/StartMiningCommandHandlerTests.cs
new file mode 100644
index 00000000..abd4bc97
--- /dev/null
+++ b/services/mining-service-net/tests/MiningService.UnitTests/Application/Commands/StartMiningCommandHandlerTests.cs
@@ -0,0 +1,107 @@
+using FluentAssertions;
+using MiningService.API.Application.Commands;
+using MiningService.Domain.AggregatesModel.MinerAggregate;
+using MiningService.Domain.Exceptions;
+using MiningService.Domain.SeedWork;
+using NSubstitute;
+using Xunit;
+
+namespace MiningService.UnitTests.Application.Commands;
+
+///
+/// EN: Unit tests for StartMiningCommandHandler.
+/// VI: Unit tests cho StartMiningCommandHandler.
+///
+public class StartMiningCommandHandlerTests
+{
+ private readonly IMinerRepository _minerRepository;
+ private readonly IUnitOfWork _unitOfWork;
+ private readonly StartMiningCommandHandler _handler;
+
+ public StartMiningCommandHandlerTests()
+ {
+ _unitOfWork = Substitute.For();
+ _unitOfWork.SaveEntitiesAsync(Arg.Any()).Returns(true);
+
+ _minerRepository = Substitute.For();
+ _minerRepository.UnitOfWork.Returns(_unitOfWork);
+ _handler = new StartMiningCommandHandler(_minerRepository);
+ }
+
+ [Fact]
+ public async Task Handle_ExistingMiner_StartsSession()
+ {
+ // Arrange
+ var userId = Guid.NewGuid();
+ var miner = Miner.Create(userId);
+ var command = new StartMiningCommand(userId);
+
+ _minerRepository.GetByUserIdAsync(userId, Arg.Any())
+ .Returns(miner);
+
+ // Act
+ var result = await _handler.Handle(command, CancellationToken.None);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.SessionId.Should().NotBeEmpty();
+ result.HourlyRate.Should().BeGreaterThan(0);
+
+ await _unitOfWork.Received(1).SaveEntitiesAsync(Arg.Any());
+ }
+
+ [Fact]
+ public async Task Handle_NewUser_CreatesMinerAndStartsSession()
+ {
+ // Arrange
+ var userId = Guid.NewGuid();
+ var command = new StartMiningCommand(userId);
+
+ _minerRepository.GetByUserIdAsync(userId, Arg.Any())
+ .Returns((Miner?)null);
+
+ // Act
+ var result = await _handler.Handle(command, CancellationToken.None);
+
+ // Assert
+ result.Should().NotBeNull();
+ result.SessionId.Should().NotBeEmpty();
+
+ _minerRepository.Received(1).Add(Arg.Is(m => m.UserId == userId));
+ }
+
+ [Fact]
+ public async Task Handle_SuspendedMiner_ThrowsException()
+ {
+ // Arrange
+ var userId = Guid.NewGuid();
+ var miner = Miner.Create(userId);
+ miner.Suspend();
+ var command = new StartMiningCommand(userId);
+
+ _minerRepository.GetByUserIdAsync(userId, Arg.Any())
+ .Returns(miner);
+
+ // Act & Assert
+ await Assert.ThrowsAsync(() =>
+ _handler.Handle(command, CancellationToken.None));
+ }
+
+ [Fact]
+ public async Task Handle_AlreadyMining_ThrowsException()
+ {
+ // Arrange
+ var userId = Guid.NewGuid();
+ var miner = Miner.Create(userId);
+ miner.StartMiningSession();
+ var command = new StartMiningCommand(userId);
+
+ _minerRepository.GetByUserIdAsync(userId, Arg.Any())
+ .Returns(miner);
+
+ // Act & Assert
+ await Assert.ThrowsAsync(() =>
+ _handler.Handle(command, CancellationToken.None));
+ }
+}
+
diff --git a/services/mining-service-net/tests/MiningService.UnitTests/Application/Queries/GetMinerStatusQueryHandlerTests.cs b/services/mining-service-net/tests/MiningService.UnitTests/Application/Queries/GetMinerStatusQueryHandlerTests.cs
new file mode 100644
index 00000000..90e23021
--- /dev/null
+++ b/services/mining-service-net/tests/MiningService.UnitTests/Application/Queries/GetMinerStatusQueryHandlerTests.cs
@@ -0,0 +1,83 @@
+using FluentAssertions;
+using MiningService.API.Application.Queries;
+using MiningService.Domain.AggregatesModel.MinerAggregate;
+using NSubstitute;
+using Xunit;
+
+namespace MiningService.UnitTests.Application.Queries;
+
+///
+/// EN: Unit tests for GetMinerStatusQueryHandler.
+/// VI: Unit tests cho GetMinerStatusQueryHandler.
+///
+public class GetMinerStatusQueryHandlerTests
+{
+ private readonly IMinerRepository _minerRepository;
+ private readonly GetMinerStatusQueryHandler _handler;
+
+ public GetMinerStatusQueryHandlerTests()
+ {
+ _minerRepository = Substitute.For();
+ _handler = new GetMinerStatusQueryHandler(_minerRepository);
+ }
+
+ [Fact]
+ public async Task Handle_ExistingMiner_ReturnsMinerStatus()
+ {
+ // Arrange
+ var userId = Guid.NewGuid();
+ var miner = Miner.Create(userId);
+ var query = new GetMinerStatusQuery(userId);
+
+ _minerRepository.GetByUserIdAsync(userId, Arg.Any())
+ .Returns(miner);
+
+ // Act
+ var result = await _handler.Handle(query, CancellationToken.None);
+
+ // Assert
+ result.Should().NotBeNull();
+ result!.MinerId.Should().Be(miner.Id);
+ result.Role.Should().Be("Pioneer");
+ result.TotalMinedPoints.Should().Be(0);
+ result.HourlyRate.Should().BeGreaterThan(0);
+ }
+
+ [Fact]
+ public async Task Handle_MinerNotFound_ReturnsNull()
+ {
+ // Arrange
+ var userId = Guid.NewGuid();
+ var query = new GetMinerStatusQuery(userId);
+
+ _minerRepository.GetByUserIdAsync(userId, Arg.Any())
+ .Returns((Miner?)null);
+
+ // Act
+ var result = await _handler.Handle(query, CancellationToken.None);
+
+ // Assert
+ result.Should().BeNull();
+ }
+
+ [Fact]
+ public async Task Handle_MinerWithActiveSession_ReturnsSessionInfo()
+ {
+ // Arrange
+ var userId = Guid.NewGuid();
+ var miner = Miner.Create(userId);
+ miner.StartMiningSession();
+ var query = new GetMinerStatusQuery(userId);
+
+ _minerRepository.GetByUserIdAsync(userId, Arg.Any())
+ .Returns(miner);
+
+ // Act
+ var result = await _handler.Handle(query, CancellationToken.None);
+
+ // Assert
+ result.Should().NotBeNull();
+ result!.HasActiveSession.Should().BeTrue();
+ result.SessionEndTime.Should().NotBeNull();
+ }
+}
diff --git a/services/mining-service-net/tests/MiningService.UnitTests/MiningService.UnitTests.csproj b/services/mining-service-net/tests/MiningService.UnitTests/MiningService.UnitTests.csproj
index 5f515811..f47e473d 100644
--- a/services/mining-service-net/tests/MiningService.UnitTests/MiningService.UnitTests.csproj
+++ b/services/mining-service-net/tests/MiningService.UnitTests/MiningService.UnitTests.csproj
@@ -10,6 +10,7 @@
+
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -17,7 +18,7 @@
-
+
diff --git a/services/mission-service-net/docker-compose.yml b/services/mission-service-net/docker-compose.yml
deleted file mode 100644
index f86e3446..00000000
--- a/services/mission-service-net/docker-compose.yml
+++ /dev/null
@@ -1,72 +0,0 @@
-version: '3.8'
-
-# EN: Docker Compose for local development
-# VI: Docker Compose cho phát triển local
-
-services:
- mission-api:
- build:
- context: .
- dockerfile: Dockerfile
- container_name: mission-api
- ports:
- - "5000:8080"
- environment:
- - ASPNETCORE_ENVIRONMENT=Development
- - DATABASE_URL=Host=postgres;Port=5432;Database=mission_db;Username=postgres;Password=postgres
- - REDIS_URL=redis:6379
- depends_on:
- postgres:
- condition: service_healthy
- redis:
- condition: service_healthy
- networks:
- - mission-network
- healthcheck:
- test: ["CMD", "curl", "-f", "http://localhost:8080/health/live"]
- interval: 30s
- timeout: 10s
- retries: 3
- start_period: 10s
-
- postgres:
- image: postgres:16-alpine
- container_name: mission-postgres
- environment:
- POSTGRES_USER: postgres
- POSTGRES_PASSWORD: postgres
- POSTGRES_DB: mission_db
- ports:
- - "5432:5432"
- volumes:
- - postgres_data:/var/lib/postgresql/data
- networks:
- - mission-network
- healthcheck:
- test: ["CMD-SHELL", "pg_isready -U postgres"]
- interval: 10s
- timeout: 5s
- retries: 5
-
- redis:
- image: redis:7-alpine
- container_name: mission-redis
- ports:
- - "6379:6379"
- volumes:
- - redis_data:/data
- networks:
- - mission-network
- healthcheck:
- test: ["CMD", "redis-cli", "ping"]
- interval: 10s
- timeout: 5s
- retries: 5
-
-volumes:
- postgres_data:
- redis_data:
-
-networks:
- mission-network:
- driver: bridge
diff --git a/services/mission-service-net/src/MissionService.API/MissionService.API.csproj b/services/mission-service-net/src/MissionService.API/MissionService.API.csproj
index 9cb19401..6a1d3d48 100644
--- a/services/mission-service-net/src/MissionService.API/MissionService.API.csproj
+++ b/services/mission-service-net/src/MissionService.API/MissionService.API.csproj
@@ -14,6 +14,10 @@
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
@@ -33,6 +37,9 @@
+
+
+
diff --git a/services/mission-service-net/src/MissionService.API/Program.cs b/services/mission-service-net/src/MissionService.API/Program.cs
index 5efae3fd..16e902c4 100644
--- a/services/mission-service-net/src/MissionService.API/Program.cs
+++ b/services/mission-service-net/src/MissionService.API/Program.cs
@@ -96,6 +96,22 @@ try
});
});
+ // EN: Add Authentication and Authorization / VI: Thêm Authentication và Authorization
+ builder.Services.AddAuthentication("Bearer")
+ .AddJwtBearer("Bearer", options =>
+ {
+ options.Authority = builder.Configuration["Jwt:Authority"] ?? "http://iam-service-net:8080";
+ options.Audience = builder.Configuration["Jwt:Audience"] ?? "goodgo-api";
+ options.RequireHttpsMetadata = false; // EN: Development only / VI: Chỉ development
+ options.TokenValidationParameters = new()
+ {
+ ValidateIssuer = false, // EN: IAM service validates / VI: IAM service xác thực
+ ValidateAudience = false,
+ ValidateLifetime = true
+ };
+ });
+ builder.Services.AddAuthorization();
+
var app = builder.Build();
// EN: Configure middleware pipeline / VI: Cấu hình middleware pipeline
@@ -114,6 +130,10 @@ try
app.UseCors();
app.UseRouting();
+
+ // EN: Add Authentication and Authorization / VI: Thêm Authentication và Authorization
+ app.UseAuthentication();
+ app.UseAuthorization();
// EN: Map health check endpoints / VI: Map health check endpoints
app.MapHealthChecks("/health");
diff --git a/services/mission-service-net/src/MissionService.Infrastructure/Migrations/20260117134348_InitialCreate.Designer.cs b/services/mission-service-net/src/MissionService.Infrastructure/Migrations/20260117134348_InitialCreate.Designer.cs
new file mode 100644
index 00000000..8b81457a
--- /dev/null
+++ b/services/mission-service-net/src/MissionService.Infrastructure/Migrations/20260117134348_InitialCreate.Designer.cs
@@ -0,0 +1,740 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using MissionService.Infrastructure;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace MissionService.Infrastructure.Migrations
+{
+ [DbContext(typeof(MissionDbContext))]
+ [Migration("20260117134348_InitialCreate")]
+ partial class InitialCreate
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.2")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.CheckInAggregate.CheckInDay", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Date")
+ .HasColumnType("date")
+ .HasColumnName("date");
+
+ b.Property("IsMilestone")
+ .HasColumnType("boolean")
+ .HasColumnName("is_milestone");
+
+ b.Property("PointsEarned")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)")
+ .HasColumnName("points_earned");
+
+ b.Property("StreakOnDay")
+ .HasColumnType("integer")
+ .HasColumnName("streak_on_day");
+
+ b.Property("user_checkin_id")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("user_checkin_id", "Date")
+ .IsUnique();
+
+ b.ToTable("checkin_days", (string)null);
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.CheckInAggregate.UserCheckIn", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CurrentStreak")
+ .HasColumnType("integer")
+ .HasColumnName("current_streak");
+
+ b.Property("LastCheckInDate")
+ .HasColumnType("date")
+ .HasColumnName("last_checkin_date");
+
+ b.Property("LongestStreak")
+ .HasColumnType("integer")
+ .HasColumnName("longest_streak");
+
+ b.Property("TotalCheckIns")
+ .HasColumnType("integer")
+ .HasColumnName("total_checkins");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_checkins", (string)null);
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.MissionAggregate.FrequencyType", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("name");
+
+ b.HasKey("Id");
+
+ b.ToTable("frequency_types", (string)null);
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ Name = "Once"
+ },
+ new
+ {
+ Id = 2,
+ Name = "Daily"
+ },
+ new
+ {
+ Id = 3,
+ Name = "Weekly"
+ },
+ new
+ {
+ Id = 4,
+ Name = "Unlimited"
+ });
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.MissionAggregate.Mission", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Category")
+ .HasColumnType("integer")
+ .HasColumnName("category_id");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("code");
+
+ b.Property("DescriptionEn")
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)")
+ .HasColumnName("description_en");
+
+ b.Property("DescriptionVi")
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)")
+ .HasColumnName("description_vi");
+
+ b.Property("EndDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("end_date");
+
+ b.Property("Frequency")
+ .HasColumnType("integer")
+ .HasColumnName("frequency_id");
+
+ b.Property("MaxCompletions")
+ .HasColumnType("integer")
+ .HasColumnName("max_completions");
+
+ b.Property("Priority")
+ .HasColumnType("integer")
+ .HasColumnName("priority");
+
+ b.Property("StartDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start_date");
+
+ b.Property("Status")
+ .HasColumnType("integer")
+ .HasColumnName("status_id");
+
+ b.Property("TitleEn")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("title_en");
+
+ b.Property("TitleVi")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("title_vi");
+
+ b.Property("Type")
+ .HasColumnType("integer")
+ .HasColumnName("type_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.ToTable("missions", (string)null);
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.MissionAggregate.MissionCategory", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("name");
+
+ b.HasKey("Id");
+
+ b.ToTable("mission_categories", (string)null);
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ Name = "Daily"
+ },
+ new
+ {
+ Id = 2,
+ Name = "Weekly"
+ },
+ new
+ {
+ Id = 3,
+ Name = "Special"
+ },
+ new
+ {
+ Id = 4,
+ Name = "Onboarding"
+ },
+ new
+ {
+ Id = 5,
+ Name = "Event"
+ });
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.MissionAggregate.MissionRule", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Metadata")
+ .HasColumnType("jsonb")
+ .HasColumnName("metadata");
+
+ b.Property("Operator")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasColumnName("operator");
+
+ b.Property("RuleType")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("rule_type");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)")
+ .HasColumnName("value");
+
+ b.Property("mission_id")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("mission_id");
+
+ b.ToTable("mission_rules", (string)null);
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.MissionAggregate.MissionStatus", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("name");
+
+ b.HasKey("Id");
+
+ b.ToTable("mission_statuses", (string)null);
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ Name = "Draft"
+ },
+ new
+ {
+ Id = 2,
+ Name = "Active"
+ },
+ new
+ {
+ Id = 3,
+ Name = "Paused"
+ },
+ new
+ {
+ Id = 4,
+ Name = "Expired"
+ },
+ new
+ {
+ Id = 5,
+ Name = "Archived"
+ });
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.MissionAggregate.MissionType", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("name");
+
+ b.HasKey("Id");
+
+ b.ToTable("mission_types", (string)null);
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ Name = "Video"
+ },
+ new
+ {
+ Id = 2,
+ Name = "Click"
+ },
+ new
+ {
+ Id = 3,
+ Name = "Upload"
+ },
+ new
+ {
+ Id = 4,
+ Name = "Invite"
+ },
+ new
+ {
+ Id = 5,
+ Name = "CheckIn"
+ },
+ new
+ {
+ Id = 6,
+ Name = "Social"
+ });
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.RewardAggregate.UserReward", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("ClaimedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("claimed_at");
+
+ b.Property("EarnedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("earned_at");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expires_at");
+
+ b.Property("SourceId")
+ .HasColumnType("uuid")
+ .HasColumnName("source_id");
+
+ b.Property("Status")
+ .HasColumnType("integer")
+ .HasColumnName("status");
+
+ b.Property("Type")
+ .HasColumnType("integer")
+ .HasColumnName("type");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("SourceId")
+ .IsUnique();
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("Status", "ExpiresAt");
+
+ b.ToTable("user_rewards", (string)null);
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.TaskAggregate.TaskStatus", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("name");
+
+ b.HasKey("Id");
+
+ b.ToTable("task_statuses", (string)null);
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ Name = "Pending"
+ },
+ new
+ {
+ Id = 2,
+ Name = "InProgress"
+ },
+ new
+ {
+ Id = 3,
+ Name = "PendingVerification"
+ },
+ new
+ {
+ Id = 4,
+ Name = "Completed"
+ },
+ new
+ {
+ Id = 5,
+ Name = "Rejected"
+ },
+ new
+ {
+ Id = 6,
+ Name = "Cancelled"
+ },
+ new
+ {
+ Id = 7,
+ Name = "Expired"
+ });
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.TaskAggregate.UserTask", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("ClaimedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("claimed_at");
+
+ b.Property("CompletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("completed_at");
+
+ b.Property("MissionId")
+ .HasColumnType("uuid")
+ .HasColumnName("mission_id");
+
+ b.Property("RewardClaimed")
+ .HasColumnType("boolean")
+ .HasColumnName("reward_claimed");
+
+ b.Property("StartedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("started_at");
+
+ b.Property("Status")
+ .HasColumnType("integer")
+ .HasColumnName("status_id");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("MissionId");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("UserId", "MissionId");
+
+ b.ToTable("user_tasks", (string)null);
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.CheckInAggregate.CheckInDay", b =>
+ {
+ b.HasOne("MissionService.Domain.AggregatesModel.CheckInAggregate.UserCheckIn", null)
+ .WithMany("CheckInDays")
+ .HasForeignKey("user_checkin_id")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.MissionAggregate.Mission", b =>
+ {
+ b.OwnsOne("MissionService.Domain.AggregatesModel.MissionAggregate.MissionReward", "Reward", b1 =>
+ {
+ b1.Property("MissionId")
+ .HasColumnType("uuid");
+
+ b1.Property("BadgeId")
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("reward_badge_id");
+
+ b1.Property("ExperiencePoints")
+ .HasColumnType("integer")
+ .HasColumnName("reward_xp");
+
+ b1.Property("MiningBoostPercent")
+ .HasPrecision(5, 2)
+ .HasColumnType("numeric(5,2)")
+ .HasColumnName("reward_mining_boost");
+
+ b1.Property("Points")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)")
+ .HasColumnName("reward_points");
+
+ b1.HasKey("MissionId");
+
+ b1.ToTable("missions");
+
+ b1.WithOwner()
+ .HasForeignKey("MissionId");
+ });
+
+ b.Navigation("Reward")
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.MissionAggregate.MissionRule", b =>
+ {
+ b.HasOne("MissionService.Domain.AggregatesModel.MissionAggregate.Mission", null)
+ .WithMany("Rules")
+ .HasForeignKey("mission_id")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.RewardAggregate.UserReward", b =>
+ {
+ b.OwnsOne("MissionService.Domain.AggregatesModel.RewardAggregate.RewardAmount", "Amount", b1 =>
+ {
+ b1.Property("UserRewardId")
+ .HasColumnType("uuid");
+
+ b1.Property("BonusPoints")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)")
+ .HasColumnName("bonus_points");
+
+ b1.Property("Currency")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)")
+ .HasDefaultValue("MP")
+ .HasColumnName("currency");
+
+ b1.Property("Points")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)")
+ .HasColumnName("points");
+
+ b1.HasKey("UserRewardId");
+
+ b1.ToTable("user_rewards");
+
+ b1.WithOwner()
+ .HasForeignKey("UserRewardId");
+ });
+
+ b.Navigation("Amount")
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.TaskAggregate.UserTask", b =>
+ {
+ b.OwnsOne("MissionService.Domain.AggregatesModel.TaskAggregate.TaskEvidence", "Evidence", b1 =>
+ {
+ b1.Property("UserTaskId")
+ .HasColumnType("uuid");
+
+ b1.Property("CapturedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("evidence_captured_at");
+
+ b1.Property("Data")
+ .IsRequired()
+ .HasMaxLength(4000)
+ .HasColumnType("character varying(4000)")
+ .HasColumnName("evidence_data");
+
+ b1.Property("ScreenshotUrl")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)")
+ .HasColumnName("evidence_screenshot_url");
+
+ b1.Property("Type")
+ .HasColumnType("integer")
+ .HasColumnName("evidence_type");
+
+ b1.Property("VideoUrl")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)")
+ .HasColumnName("evidence_video_url");
+
+ b1.HasKey("UserTaskId");
+
+ b1.ToTable("user_tasks");
+
+ b1.WithOwner()
+ .HasForeignKey("UserTaskId");
+ });
+
+ b.OwnsOne("MissionService.Domain.AggregatesModel.TaskAggregate.TaskProgress", "Progress", b1 =>
+ {
+ b1.Property("UserTaskId")
+ .HasColumnType("uuid");
+
+ b1.Property("CurrentValue")
+ .HasColumnType("integer")
+ .HasColumnName("progress_current");
+
+ b1.Property("LastUpdated")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("progress_updated_at");
+
+ b1.Property("TargetValue")
+ .HasColumnType("integer")
+ .HasColumnName("progress_target");
+
+ b1.HasKey("UserTaskId");
+
+ b1.ToTable("user_tasks");
+
+ b1.WithOwner()
+ .HasForeignKey("UserTaskId");
+ });
+
+ b.OwnsOne("MissionService.Domain.AggregatesModel.TaskAggregate.VerificationResult", "Verification", b1 =>
+ {
+ b1.Property("UserTaskId")
+ .HasColumnType("uuid");
+
+ b1.Property("FailureReason")
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)")
+ .HasColumnName("verification_failure_reason");
+
+ b1.Property("IsValid")
+ .HasColumnType("boolean")
+ .HasColumnName("verification_valid");
+
+ b1.Property("Method")
+ .HasColumnType("integer")
+ .HasColumnName("verification_method");
+
+ b1.Property("VerifiedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("verification_at");
+
+ b1.Property("VerifiedBy")
+ .HasColumnType("uuid")
+ .HasColumnName("verification_by");
+
+ b1.HasKey("UserTaskId");
+
+ b1.ToTable("user_tasks");
+
+ b1.WithOwner()
+ .HasForeignKey("UserTaskId");
+ });
+
+ b.Navigation("Evidence");
+
+ b.Navigation("Progress")
+ .IsRequired();
+
+ b.Navigation("Verification");
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.CheckInAggregate.UserCheckIn", b =>
+ {
+ b.Navigation("CheckInDays");
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.MissionAggregate.Mission", b =>
+ {
+ b.Navigation("Rules");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/services/mission-service-net/src/MissionService.Infrastructure/Migrations/20260117134348_InitialCreate.cs b/services/mission-service-net/src/MissionService.Infrastructure/Migrations/20260117134348_InitialCreate.cs
new file mode 100644
index 00000000..a8e0a348
--- /dev/null
+++ b/services/mission-service-net/src/MissionService.Infrastructure/Migrations/20260117134348_InitialCreate.cs
@@ -0,0 +1,370 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
+
+namespace MissionService.Infrastructure.Migrations
+{
+ ///
+ public partial class InitialCreate : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "frequency_types",
+ columns: table => new
+ {
+ id = table.Column(type: "integer", nullable: false),
+ name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_frequency_types", x => x.id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "mission_categories",
+ columns: table => new
+ {
+ id = table.Column(type: "integer", nullable: false),
+ name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_mission_categories", x => x.id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "mission_statuses",
+ columns: table => new
+ {
+ id = table.Column(type: "integer", nullable: false),
+ name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_mission_statuses", x => x.id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "mission_types",
+ columns: table => new
+ {
+ id = table.Column(type: "integer", nullable: false),
+ name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_mission_types", x => x.id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "missions",
+ columns: table => new
+ {
+ id = table.Column(type: "uuid", nullable: false),
+ code = table.Column(type: "character varying(100)", maxLength: 100, nullable: false),
+ title_en = table.Column(type: "character varying(200)", maxLength: 200, nullable: false),
+ title_vi = table.Column(type: "character varying(200)", maxLength: 200, nullable: false),
+ description_en = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true),
+ description_vi = table.Column(type: "character varying(2000)", maxLength: 2000, nullable: true),
+ type_id = table.Column(type: "integer", nullable: false),
+ category_id = table.Column(type: "integer", nullable: false),
+ reward_points = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
+ reward_mining_boost = table.Column(type: "numeric(5,2)", precision: 5, scale: 2, nullable: false),
+ reward_xp = table.Column(type: "integer", nullable: false),
+ reward_badge_id = table.Column(type: "character varying(50)", maxLength: 50, nullable: true),
+ frequency_id = table.Column(type: "integer", nullable: false),
+ max_completions = table.Column(type: "integer", nullable: false),
+ start_date = table.Column(type: "timestamp with time zone", nullable: false),
+ end_date = table.Column(type: "timestamp with time zone", nullable: true),
+ status_id = table.Column(type: "integer", nullable: false),
+ priority = table.Column(type: "integer", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_missions", x => x.id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "task_statuses",
+ columns: table => new
+ {
+ id = table.Column(type: "integer", nullable: false),
+ name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_task_statuses", x => x.id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "user_checkins",
+ columns: table => new
+ {
+ id = table.Column(type: "uuid", nullable: false),
+ user_id = table.Column(type: "uuid", nullable: false),
+ current_streak = table.Column(type: "integer", nullable: false),
+ longest_streak = table.Column(type: "integer", nullable: false),
+ total_checkins = table.Column(type: "integer", nullable: false),
+ last_checkin_date = table.Column(type: "date", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_user_checkins", x => x.id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "user_rewards",
+ columns: table => new
+ {
+ id = table.Column(type: "uuid", nullable: false),
+ user_id = table.Column(type: "uuid", nullable: false),
+ source_id = table.Column(type: "uuid", nullable: false),
+ type = table.Column(type: "integer", nullable: false),
+ status = table.Column(type: "integer", nullable: false),
+ points = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
+ bonus_points = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
+ currency = table.Column(type: "character varying(10)", maxLength: 10, nullable: false, defaultValue: "MP"),
+ earned_at = table.Column(type: "timestamp with time zone", nullable: false),
+ claimed_at = table.Column(type: "timestamp with time zone", nullable: true),
+ expires_at = table.Column(type: "timestamp with time zone", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_user_rewards", x => x.id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "user_tasks",
+ columns: table => new
+ {
+ id = table.Column(type: "uuid", nullable: false),
+ user_id = table.Column(type: "uuid", nullable: false),
+ mission_id = table.Column(type: "uuid", nullable: false),
+ status_id = table.Column(type: "integer", nullable: false),
+ progress_current = table.Column(type: "integer", nullable: false),
+ progress_target = table.Column(type: "integer", nullable: false),
+ progress_updated_at = table.Column(type: "timestamp with time zone", nullable: false),
+ evidence_type = table.Column(type: "integer", nullable: true),
+ evidence_data = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: true),
+ evidence_screenshot_url = table.Column(type: "character varying(500)", maxLength: 500, nullable: true),
+ evidence_video_url = table.Column(type: "character varying(500)", maxLength: 500, nullable: true),
+ evidence_captured_at = table.Column(type: "timestamp with time zone", nullable: true),
+ verification_valid = table.Column(type: "boolean", nullable: true),
+ verification_failure_reason = table.Column(type: "character varying(500)", maxLength: 500, nullable: true),
+ verification_method = table.Column(type: "integer", nullable: true),
+ verification_by = table.Column(type: "uuid", nullable: true),
+ verification_at = table.Column(type: "timestamp with time zone", nullable: true),
+ reward_claimed = table.Column(type: "boolean", nullable: false),
+ started_at = table.Column(type: "timestamp with time zone", nullable: false),
+ completed_at = table.Column(type: "timestamp with time zone", nullable: true),
+ claimed_at = table.Column(type: "timestamp with time zone", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_user_tasks", x => x.id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "mission_rules",
+ columns: table => new
+ {
+ id = table.Column(type: "uuid", nullable: false),
+ rule_type = table.Column(type: "character varying(50)", maxLength: 50, nullable: false),
+ @operator = table.Column(name: "operator", type: "character varying(20)", maxLength: 20, nullable: false),
+ value = table.Column(type: "character varying(500)", maxLength: 500, nullable: false),
+ metadata = table.Column(type: "jsonb", nullable: true),
+ mission_id = table.Column(type: "uuid", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_mission_rules", x => x.id);
+ table.ForeignKey(
+ name: "FK_mission_rules_missions_mission_id",
+ column: x => x.mission_id,
+ principalTable: "missions",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "checkin_days",
+ columns: table => new
+ {
+ id = table.Column(type: "uuid", nullable: false),
+ date = table.Column(type: "date", nullable: false),
+ points_earned = table.Column(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
+ is_milestone = table.Column(type: "boolean", nullable: false),
+ streak_on_day = table.Column(type: "integer", nullable: false),
+ user_checkin_id = table.Column(type: "uuid", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_checkin_days", x => x.id);
+ table.ForeignKey(
+ name: "FK_checkin_days_user_checkins_user_checkin_id",
+ column: x => x.user_checkin_id,
+ principalTable: "user_checkins",
+ principalColumn: "id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.InsertData(
+ table: "frequency_types",
+ columns: new[] { "id", "name" },
+ values: new object[,]
+ {
+ { 1, "Once" },
+ { 2, "Daily" },
+ { 3, "Weekly" },
+ { 4, "Unlimited" }
+ });
+
+ migrationBuilder.InsertData(
+ table: "mission_categories",
+ columns: new[] { "id", "name" },
+ values: new object[,]
+ {
+ { 1, "Daily" },
+ { 2, "Weekly" },
+ { 3, "Special" },
+ { 4, "Onboarding" },
+ { 5, "Event" }
+ });
+
+ migrationBuilder.InsertData(
+ table: "mission_statuses",
+ columns: new[] { "id", "name" },
+ values: new object[,]
+ {
+ { 1, "Draft" },
+ { 2, "Active" },
+ { 3, "Paused" },
+ { 4, "Expired" },
+ { 5, "Archived" }
+ });
+
+ migrationBuilder.InsertData(
+ table: "mission_types",
+ columns: new[] { "id", "name" },
+ values: new object[,]
+ {
+ { 1, "Video" },
+ { 2, "Click" },
+ { 3, "Upload" },
+ { 4, "Invite" },
+ { 5, "CheckIn" },
+ { 6, "Social" }
+ });
+
+ migrationBuilder.InsertData(
+ table: "task_statuses",
+ columns: new[] { "id", "name" },
+ values: new object[,]
+ {
+ { 1, "Pending" },
+ { 2, "InProgress" },
+ { 3, "PendingVerification" },
+ { 4, "Completed" },
+ { 5, "Rejected" },
+ { 6, "Cancelled" },
+ { 7, "Expired" }
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_checkin_days_user_checkin_id_date",
+ table: "checkin_days",
+ columns: new[] { "user_checkin_id", "date" },
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_mission_rules_mission_id",
+ table: "mission_rules",
+ column: "mission_id");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_missions_code",
+ table: "missions",
+ column: "code",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_user_checkins_user_id",
+ table: "user_checkins",
+ column: "user_id",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_user_rewards_source_id",
+ table: "user_rewards",
+ column: "source_id",
+ unique: true);
+
+ migrationBuilder.CreateIndex(
+ name: "IX_user_rewards_status_expires_at",
+ table: "user_rewards",
+ columns: new[] { "status", "expires_at" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_user_rewards_user_id",
+ table: "user_rewards",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_user_tasks_mission_id",
+ table: "user_tasks",
+ column: "mission_id");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_user_tasks_user_id",
+ table: "user_tasks",
+ column: "user_id");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_user_tasks_user_id_mission_id",
+ table: "user_tasks",
+ columns: new[] { "user_id", "mission_id" });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "checkin_days");
+
+ migrationBuilder.DropTable(
+ name: "frequency_types");
+
+ migrationBuilder.DropTable(
+ name: "mission_categories");
+
+ migrationBuilder.DropTable(
+ name: "mission_rules");
+
+ migrationBuilder.DropTable(
+ name: "mission_statuses");
+
+ migrationBuilder.DropTable(
+ name: "mission_types");
+
+ migrationBuilder.DropTable(
+ name: "task_statuses");
+
+ migrationBuilder.DropTable(
+ name: "user_rewards");
+
+ migrationBuilder.DropTable(
+ name: "user_tasks");
+
+ migrationBuilder.DropTable(
+ name: "user_checkins");
+
+ migrationBuilder.DropTable(
+ name: "missions");
+ }
+ }
+}
diff --git a/services/mission-service-net/src/MissionService.Infrastructure/Migrations/MissionDbContextModelSnapshot.cs b/services/mission-service-net/src/MissionService.Infrastructure/Migrations/MissionDbContextModelSnapshot.cs
new file mode 100644
index 00000000..e7527402
--- /dev/null
+++ b/services/mission-service-net/src/MissionService.Infrastructure/Migrations/MissionDbContextModelSnapshot.cs
@@ -0,0 +1,737 @@
+//
+using System;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using MissionService.Infrastructure;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace MissionService.Infrastructure.Migrations
+{
+ [DbContext(typeof(MissionDbContext))]
+ partial class MissionDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.2")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.CheckInAggregate.CheckInDay", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Date")
+ .HasColumnType("date")
+ .HasColumnName("date");
+
+ b.Property("IsMilestone")
+ .HasColumnType("boolean")
+ .HasColumnName("is_milestone");
+
+ b.Property("PointsEarned")
+ .HasPrecision(18, 2)
+ .HasColumnType("numeric(18,2)")
+ .HasColumnName("points_earned");
+
+ b.Property("StreakOnDay")
+ .HasColumnType("integer")
+ .HasColumnName("streak_on_day");
+
+ b.Property("user_checkin_id")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("user_checkin_id", "Date")
+ .IsUnique();
+
+ b.ToTable("checkin_days", (string)null);
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.CheckInAggregate.UserCheckIn", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("CurrentStreak")
+ .HasColumnType("integer")
+ .HasColumnName("current_streak");
+
+ b.Property("LastCheckInDate")
+ .HasColumnType("date")
+ .HasColumnName("last_checkin_date");
+
+ b.Property("LongestStreak")
+ .HasColumnType("integer")
+ .HasColumnName("longest_streak");
+
+ b.Property("TotalCheckIns")
+ .HasColumnType("integer")
+ .HasColumnName("total_checkins");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.ToTable("user_checkins", (string)null);
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.MissionAggregate.FrequencyType", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("name");
+
+ b.HasKey("Id");
+
+ b.ToTable("frequency_types", (string)null);
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ Name = "Once"
+ },
+ new
+ {
+ Id = 2,
+ Name = "Daily"
+ },
+ new
+ {
+ Id = 3,
+ Name = "Weekly"
+ },
+ new
+ {
+ Id = 4,
+ Name = "Unlimited"
+ });
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.MissionAggregate.Mission", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Category")
+ .HasColumnType("integer")
+ .HasColumnName("category_id");
+
+ b.Property("Code")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)")
+ .HasColumnName("code");
+
+ b.Property("DescriptionEn")
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)")
+ .HasColumnName("description_en");
+
+ b.Property("DescriptionVi")
+ .HasMaxLength(2000)
+ .HasColumnType("character varying(2000)")
+ .HasColumnName("description_vi");
+
+ b.Property("EndDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("end_date");
+
+ b.Property("Frequency")
+ .HasColumnType("integer")
+ .HasColumnName("frequency_id");
+
+ b.Property("MaxCompletions")
+ .HasColumnType("integer")
+ .HasColumnName("max_completions");
+
+ b.Property("Priority")
+ .HasColumnType("integer")
+ .HasColumnName("priority");
+
+ b.Property("StartDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start_date");
+
+ b.Property("Status")
+ .HasColumnType("integer")
+ .HasColumnName("status_id");
+
+ b.Property("TitleEn")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("title_en");
+
+ b.Property("TitleVi")
+ .IsRequired()
+ .HasMaxLength(200)
+ .HasColumnType("character varying(200)")
+ .HasColumnName("title_vi");
+
+ b.Property("Type")
+ .HasColumnType("integer")
+ .HasColumnName("type_id");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Code")
+ .IsUnique();
+
+ b.ToTable("missions", (string)null);
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.MissionAggregate.MissionCategory", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("name");
+
+ b.HasKey("Id");
+
+ b.ToTable("mission_categories", (string)null);
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ Name = "Daily"
+ },
+ new
+ {
+ Id = 2,
+ Name = "Weekly"
+ },
+ new
+ {
+ Id = 3,
+ Name = "Special"
+ },
+ new
+ {
+ Id = 4,
+ Name = "Onboarding"
+ },
+ new
+ {
+ Id = 5,
+ Name = "Event"
+ });
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.MissionAggregate.MissionRule", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("Metadata")
+ .HasColumnType("jsonb")
+ .HasColumnName("metadata");
+
+ b.Property("Operator")
+ .IsRequired()
+ .HasMaxLength(20)
+ .HasColumnType("character varying(20)")
+ .HasColumnName("operator");
+
+ b.Property("RuleType")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("rule_type");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasMaxLength(500)
+ .HasColumnType("character varying(500)")
+ .HasColumnName("value");
+
+ b.Property("mission_id")
+ .HasColumnType("uuid");
+
+ b.HasKey("Id");
+
+ b.HasIndex("mission_id");
+
+ b.ToTable("mission_rules", (string)null);
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.MissionAggregate.MissionStatus", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("name");
+
+ b.HasKey("Id");
+
+ b.ToTable("mission_statuses", (string)null);
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ Name = "Draft"
+ },
+ new
+ {
+ Id = 2,
+ Name = "Active"
+ },
+ new
+ {
+ Id = 3,
+ Name = "Paused"
+ },
+ new
+ {
+ Id = 4,
+ Name = "Expired"
+ },
+ new
+ {
+ Id = 5,
+ Name = "Archived"
+ });
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.MissionAggregate.MissionType", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("integer")
+ .HasColumnName("id");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(50)
+ .HasColumnType("character varying(50)")
+ .HasColumnName("name");
+
+ b.HasKey("Id");
+
+ b.ToTable("mission_types", (string)null);
+
+ b.HasData(
+ new
+ {
+ Id = 1,
+ Name = "Video"
+ },
+ new
+ {
+ Id = 2,
+ Name = "Click"
+ },
+ new
+ {
+ Id = 3,
+ Name = "Upload"
+ },
+ new
+ {
+ Id = 4,
+ Name = "Invite"
+ },
+ new
+ {
+ Id = 5,
+ Name = "CheckIn"
+ },
+ new
+ {
+ Id = 6,
+ Name = "Social"
+ });
+ });
+
+ modelBuilder.Entity("MissionService.Domain.AggregatesModel.RewardAggregate.UserReward", b =>
+ {
+ b.Property("Id")
+ .HasColumnType("uuid")
+ .HasColumnName("id");
+
+ b.Property("ClaimedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("claimed_at");
+
+ b.Property("EarnedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("earned_at");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expires_at");
+
+ b.Property("SourceId")
+ .HasColumnType("uuid")
+ .HasColumnName("source_id");
+
+ b.Property("Status")
+ .HasColumnType("integer")
+ .HasColumnName("status");
+
+ b.Property