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("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 + } + } +}