feat: Initialize MissionService database schema, add MiningService unit tests, and update deployment configurations.

This commit is contained in:
Ho Ngoc Hai
2026-01-17 20:49:24 +07:00
parent 1dfd72a10a
commit 35dac2e49e
15 changed files with 2445 additions and 77 deletions

View File

@@ -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:

View File

@@ -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 |
---

View File

@@ -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 |
---

View File

@@ -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;
/// <summary>
/// EN: Unit tests for Admin Command Handlers.
/// VI: Unit tests cho Admin Command Handlers.
/// </summary>
public class AdminCommandHandlerTests
{
private readonly IMinerRepository _minerRepository;
public AdminCommandHandlerTests()
{
_minerRepository = Substitute.For<IMinerRepository>();
}
#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<CancellationToken>())
.Returns(miner);
_minerRepository.UnitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>())
.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<CancellationToken>())
.Returns((Miner?)null);
// Act & Assert
await Assert.ThrowsAsync<MinerNotFoundException>(() =>
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<CancellationToken>())
.Returns(miner);
_minerRepository.UnitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>())
.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<CancellationToken>())
.Returns((Miner?)null);
// Act & Assert
await Assert.ThrowsAsync<MinerNotFoundException>(() =>
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<CancellationToken>())
.Returns(miner);
_minerRepository.UnitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>())
.Returns(true);
// Act
var result = await handler.Handle(command, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result.Success.Should().BeTrue();
}
#endregion
}

View File

@@ -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;
/// <summary>
/// EN: Unit tests for Circle Command Handlers.
/// VI: Unit tests cho Circle Command Handlers.
/// </summary>
public class CircleCommandHandlerTests
{
private readonly ICircleRepository _circleRepository;
private readonly IMinerRepository _minerRepository;
public CircleCommandHandlerTests()
{
_circleRepository = Substitute.For<ICircleRepository>();
_minerRepository = Substitute.For<IMinerRepository>();
}
#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<CancellationToken>())
.Returns(miner);
_circleRepository.UnitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>())
.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<Circle>(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<CancellationToken>())
.Returns(miner);
// Act & Assert
var act = () => handler.Handle(command, CancellationToken.None);
await act.Should().ThrowAsync<Exception>().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<CancellationToken>())
.Returns(circle);
_minerRepository.GetByUserIdAsync(ownerId, Arg.Any<CancellationToken>())
.Returns(inviter);
_minerRepository.GetByUserIdAsync(inviteeId, Arg.Any<CancellationToken>())
.Returns(invitee);
_circleRepository.UnitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>())
.Returns(true);
// Act
var result = await handler.Handle(command, CancellationToken.None);
// Assert
result.Should().BeTrue();
}
#endregion
}

View File

@@ -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;
/// <summary>
/// EN: Unit tests for ClaimMiningRewardCommandHandler.
/// VI: Unit tests cho ClaimMiningRewardCommandHandler.
/// </summary>
public class ClaimMiningRewardCommandHandlerTests
{
private readonly IMinerRepository _minerRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly ClaimMiningRewardCommandHandler _handler;
public ClaimMiningRewardCommandHandlerTests()
{
_unitOfWork = Substitute.For<IUnitOfWork>();
_minerRepository = Substitute.For<IMinerRepository>();
_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<CancellationToken>())
.Returns(miner);
_minerRepository.UnitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>())
.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<CancellationToken>());
}
[Fact]
public async Task Handle_MinerNotFound_ThrowsNotFoundException()
{
// Arrange
var userId = Guid.NewGuid();
var command = new ClaimMiningRewardCommand(userId);
_minerRepository.GetByUserIdAsync(userId, Arg.Any<CancellationToken>())
.Returns((Miner?)null);
// Act & Assert
await Assert.ThrowsAsync<MinerNotFoundException>(() =>
_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<CancellationToken>())
.Returns(miner);
// Act & Assert
await Assert.ThrowsAsync<MiningDomainException>(() =>
_handler.Handle(command, CancellationToken.None));
}
}

View File

@@ -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;
/// <summary>
/// EN: Unit tests for StartMiningCommandHandler.
/// VI: Unit tests cho StartMiningCommandHandler.
/// </summary>
public class StartMiningCommandHandlerTests
{
private readonly IMinerRepository _minerRepository;
private readonly IUnitOfWork _unitOfWork;
private readonly StartMiningCommandHandler _handler;
public StartMiningCommandHandlerTests()
{
_unitOfWork = Substitute.For<IUnitOfWork>();
_unitOfWork.SaveEntitiesAsync(Arg.Any<CancellationToken>()).Returns(true);
_minerRepository = Substitute.For<IMinerRepository>();
_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<CancellationToken>())
.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<CancellationToken>());
}
[Fact]
public async Task Handle_NewUser_CreatesMinerAndStartsSession()
{
// Arrange
var userId = Guid.NewGuid();
var command = new StartMiningCommand(userId);
_minerRepository.GetByUserIdAsync(userId, Arg.Any<CancellationToken>())
.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<Miner>(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<CancellationToken>())
.Returns(miner);
// Act & Assert
await Assert.ThrowsAsync<MiningDomainException>(() =>
_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<CancellationToken>())
.Returns(miner);
// Act & Assert
await Assert.ThrowsAsync<MiningDomainException>(() =>
_handler.Handle(command, CancellationToken.None));
}
}

View File

@@ -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;
/// <summary>
/// EN: Unit tests for GetMinerStatusQueryHandler.
/// VI: Unit tests cho GetMinerStatusQueryHandler.
/// </summary>
public class GetMinerStatusQueryHandlerTests
{
private readonly IMinerRepository _minerRepository;
private readonly GetMinerStatusQueryHandler _handler;
public GetMinerStatusQueryHandlerTests()
{
_minerRepository = Substitute.For<IMinerRepository>();
_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<CancellationToken>())
.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<CancellationToken>())
.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<CancellationToken>())
.Returns(miner);
// Act
var result = await _handler.Handle(query, CancellationToken.None);
// Assert
result.Should().NotBeNull();
result!.HasActiveSession.Should().BeTrue();
result.SessionEndTime.Should().NotBeNull();
}
}

View File

@@ -10,6 +10,7 @@
<ItemGroup>
<!-- EN: Test framework / VI: Test framework -->
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NSubstitute" Version="5.3.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -17,7 +18,7 @@
</PackageReference>
<!-- EN: Assertions and mocking / VI: Assertions và mocking -->
<PackageReference Include="FluentAssertions" Version="6.12.2" />
<PackageReference Include="FluentAssertions" Version="8.8.0" />
<PackageReference Include="Moq" Version="4.20.72" />
<!-- EN: Coverage / VI: Coverage -->

View File

@@ -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

View File

@@ -14,6 +14,10 @@
<!-- EN: FluentValidation for request validation / VI: FluentValidation cho validation request -->
<PackageReference Include="FluentValidation" Version="11.11.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<!-- EN: Swagger/OpenAPI / VI: Swagger/OpenAPI -->
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
@@ -33,6 +37,9 @@
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.Seq" Version="8.0.0" />
<!-- EN: JWT Authentication / VI: JWT Authentication -->
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -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");

View File

@@ -0,0 +1,740 @@
// <auto-generated />
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
{
/// <inheritdoc />
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<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateOnly>("Date")
.HasColumnType("date")
.HasColumnName("date");
b.Property<bool>("IsMilestone")
.HasColumnType("boolean")
.HasColumnName("is_milestone");
b.Property<decimal>("PointsEarned")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)")
.HasColumnName("points_earned");
b.Property<int>("StreakOnDay")
.HasColumnType("integer")
.HasColumnName("streak_on_day");
b.Property<Guid>("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<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<int>("CurrentStreak")
.HasColumnType("integer")
.HasColumnName("current_streak");
b.Property<DateOnly?>("LastCheckInDate")
.HasColumnType("date")
.HasColumnName("last_checkin_date");
b.Property<int>("LongestStreak")
.HasColumnType("integer")
.HasColumnName("longest_streak");
b.Property<int>("TotalCheckIns")
.HasColumnType("integer")
.HasColumnName("total_checkins");
b.Property<Guid>("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<int>("Id")
.HasColumnType("integer")
.HasColumnName("id");
b.Property<string>("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<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<int>("Category")
.HasColumnType("integer")
.HasColumnName("category_id");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("code");
b.Property<string>("DescriptionEn")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)")
.HasColumnName("description_en");
b.Property<string>("DescriptionVi")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)")
.HasColumnName("description_vi");
b.Property<DateTime?>("EndDate")
.HasColumnType("timestamp with time zone")
.HasColumnName("end_date");
b.Property<int>("Frequency")
.HasColumnType("integer")
.HasColumnName("frequency_id");
b.Property<int>("MaxCompletions")
.HasColumnType("integer")
.HasColumnName("max_completions");
b.Property<int>("Priority")
.HasColumnType("integer")
.HasColumnName("priority");
b.Property<DateTime>("StartDate")
.HasColumnType("timestamp with time zone")
.HasColumnName("start_date");
b.Property<int>("Status")
.HasColumnType("integer")
.HasColumnName("status_id");
b.Property<string>("TitleEn")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("title_en");
b.Property<string>("TitleVi")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("title_vi");
b.Property<int>("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<int>("Id")
.HasColumnType("integer")
.HasColumnName("id");
b.Property<string>("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<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<string>("Metadata")
.HasColumnType("jsonb")
.HasColumnName("metadata");
b.Property<string>("Operator")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)")
.HasColumnName("operator");
b.Property<string>("RuleType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("rule_type");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasColumnName("value");
b.Property<Guid>("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<int>("Id")
.HasColumnType("integer")
.HasColumnName("id");
b.Property<string>("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<int>("Id")
.HasColumnType("integer")
.HasColumnName("id");
b.Property<string>("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<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTime?>("ClaimedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("claimed_at");
b.Property<DateTime>("EarnedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("earned_at");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expires_at");
b.Property<Guid>("SourceId")
.HasColumnType("uuid")
.HasColumnName("source_id");
b.Property<int>("Status")
.HasColumnType("integer")
.HasColumnName("status");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");
b.Property<Guid>("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<int>("Id")
.HasColumnType("integer")
.HasColumnName("id");
b.Property<string>("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<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTime?>("ClaimedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("claimed_at");
b.Property<DateTime?>("CompletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("completed_at");
b.Property<Guid>("MissionId")
.HasColumnType("uuid")
.HasColumnName("mission_id");
b.Property<bool>("RewardClaimed")
.HasColumnType("boolean")
.HasColumnName("reward_claimed");
b.Property<DateTime>("StartedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("started_at");
b.Property<int>("Status")
.HasColumnType("integer")
.HasColumnName("status_id");
b.Property<Guid>("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<Guid>("MissionId")
.HasColumnType("uuid");
b1.Property<string>("BadgeId")
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("reward_badge_id");
b1.Property<int>("ExperiencePoints")
.HasColumnType("integer")
.HasColumnName("reward_xp");
b1.Property<decimal>("MiningBoostPercent")
.HasPrecision(5, 2)
.HasColumnType("numeric(5,2)")
.HasColumnName("reward_mining_boost");
b1.Property<decimal>("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<Guid>("UserRewardId")
.HasColumnType("uuid");
b1.Property<decimal>("BonusPoints")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)")
.HasColumnName("bonus_points");
b1.Property<string>("Currency")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(10)
.HasColumnType("character varying(10)")
.HasDefaultValue("MP")
.HasColumnName("currency");
b1.Property<decimal>("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<Guid>("UserTaskId")
.HasColumnType("uuid");
b1.Property<DateTime>("CapturedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("evidence_captured_at");
b1.Property<string>("Data")
.IsRequired()
.HasMaxLength(4000)
.HasColumnType("character varying(4000)")
.HasColumnName("evidence_data");
b1.Property<string>("ScreenshotUrl")
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasColumnName("evidence_screenshot_url");
b1.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("evidence_type");
b1.Property<string>("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<Guid>("UserTaskId")
.HasColumnType("uuid");
b1.Property<int>("CurrentValue")
.HasColumnType("integer")
.HasColumnName("progress_current");
b1.Property<DateTime>("LastUpdated")
.HasColumnType("timestamp with time zone")
.HasColumnName("progress_updated_at");
b1.Property<int>("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<Guid>("UserTaskId")
.HasColumnType("uuid");
b1.Property<string>("FailureReason")
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasColumnName("verification_failure_reason");
b1.Property<bool>("IsValid")
.HasColumnType("boolean")
.HasColumnName("verification_valid");
b1.Property<int>("Method")
.HasColumnType("integer")
.HasColumnName("verification_method");
b1.Property<DateTime>("VerifiedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("verification_at");
b1.Property<Guid?>("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
}
}
}

View File

@@ -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
{
/// <inheritdoc />
public partial class InitialCreate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "frequency_types",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false),
name = table.Column<string>(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<int>(type: "integer", nullable: false),
name = table.Column<string>(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<int>(type: "integer", nullable: false),
name = table.Column<string>(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<int>(type: "integer", nullable: false),
name = table.Column<string>(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<Guid>(type: "uuid", nullable: false),
code = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
title_en = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
title_vi = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
description_en = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
description_vi = table.Column<string>(type: "character varying(2000)", maxLength: 2000, nullable: true),
type_id = table.Column<int>(type: "integer", nullable: false),
category_id = table.Column<int>(type: "integer", nullable: false),
reward_points = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
reward_mining_boost = table.Column<decimal>(type: "numeric(5,2)", precision: 5, scale: 2, nullable: false),
reward_xp = table.Column<int>(type: "integer", nullable: false),
reward_badge_id = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: true),
frequency_id = table.Column<int>(type: "integer", nullable: false),
max_completions = table.Column<int>(type: "integer", nullable: false),
start_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
end_date = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
status_id = table.Column<int>(type: "integer", nullable: false),
priority = table.Column<int>(type: "integer", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_missions", x => x.id);
});
migrationBuilder.CreateTable(
name: "task_statuses",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false),
name = table.Column<string>(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<Guid>(type: "uuid", nullable: false),
user_id = table.Column<Guid>(type: "uuid", nullable: false),
current_streak = table.Column<int>(type: "integer", nullable: false),
longest_streak = table.Column<int>(type: "integer", nullable: false),
total_checkins = table.Column<int>(type: "integer", nullable: false),
last_checkin_date = table.Column<DateOnly>(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<Guid>(type: "uuid", nullable: false),
user_id = table.Column<Guid>(type: "uuid", nullable: false),
source_id = table.Column<Guid>(type: "uuid", nullable: false),
type = table.Column<int>(type: "integer", nullable: false),
status = table.Column<int>(type: "integer", nullable: false),
points = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
bonus_points = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
currency = table.Column<string>(type: "character varying(10)", maxLength: 10, nullable: false, defaultValue: "MP"),
earned_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
claimed_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
expires_at = table.Column<DateTime>(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<Guid>(type: "uuid", nullable: false),
user_id = table.Column<Guid>(type: "uuid", nullable: false),
mission_id = table.Column<Guid>(type: "uuid", nullable: false),
status_id = table.Column<int>(type: "integer", nullable: false),
progress_current = table.Column<int>(type: "integer", nullable: false),
progress_target = table.Column<int>(type: "integer", nullable: false),
progress_updated_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
evidence_type = table.Column<int>(type: "integer", nullable: true),
evidence_data = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: true),
evidence_screenshot_url = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
evidence_video_url = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
evidence_captured_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
verification_valid = table.Column<bool>(type: "boolean", nullable: true),
verification_failure_reason = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
verification_method = table.Column<int>(type: "integer", nullable: true),
verification_by = table.Column<Guid>(type: "uuid", nullable: true),
verification_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
reward_claimed = table.Column<bool>(type: "boolean", nullable: false),
started_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
completed_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
claimed_at = table.Column<DateTime>(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<Guid>(type: "uuid", nullable: false),
rule_type = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
@operator = table.Column<string>(name: "operator", type: "character varying(20)", maxLength: 20, nullable: false),
value = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
metadata = table.Column<string>(type: "jsonb", nullable: true),
mission_id = table.Column<Guid>(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<Guid>(type: "uuid", nullable: false),
date = table.Column<DateOnly>(type: "date", nullable: false),
points_earned = table.Column<decimal>(type: "numeric(18,2)", precision: 18, scale: 2, nullable: false),
is_milestone = table.Column<bool>(type: "boolean", nullable: false),
streak_on_day = table.Column<int>(type: "integer", nullable: false),
user_checkin_id = table.Column<Guid>(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" });
}
/// <inheritdoc />
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");
}
}
}

View File

@@ -0,0 +1,737 @@
// <auto-generated />
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<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateOnly>("Date")
.HasColumnType("date")
.HasColumnName("date");
b.Property<bool>("IsMilestone")
.HasColumnType("boolean")
.HasColumnName("is_milestone");
b.Property<decimal>("PointsEarned")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)")
.HasColumnName("points_earned");
b.Property<int>("StreakOnDay")
.HasColumnType("integer")
.HasColumnName("streak_on_day");
b.Property<Guid>("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<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<int>("CurrentStreak")
.HasColumnType("integer")
.HasColumnName("current_streak");
b.Property<DateOnly?>("LastCheckInDate")
.HasColumnType("date")
.HasColumnName("last_checkin_date");
b.Property<int>("LongestStreak")
.HasColumnType("integer")
.HasColumnName("longest_streak");
b.Property<int>("TotalCheckIns")
.HasColumnType("integer")
.HasColumnName("total_checkins");
b.Property<Guid>("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<int>("Id")
.HasColumnType("integer")
.HasColumnName("id");
b.Property<string>("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<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<int>("Category")
.HasColumnType("integer")
.HasColumnName("category_id");
b.Property<string>("Code")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("character varying(100)")
.HasColumnName("code");
b.Property<string>("DescriptionEn")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)")
.HasColumnName("description_en");
b.Property<string>("DescriptionVi")
.HasMaxLength(2000)
.HasColumnType("character varying(2000)")
.HasColumnName("description_vi");
b.Property<DateTime?>("EndDate")
.HasColumnType("timestamp with time zone")
.HasColumnName("end_date");
b.Property<int>("Frequency")
.HasColumnType("integer")
.HasColumnName("frequency_id");
b.Property<int>("MaxCompletions")
.HasColumnType("integer")
.HasColumnName("max_completions");
b.Property<int>("Priority")
.HasColumnType("integer")
.HasColumnName("priority");
b.Property<DateTime>("StartDate")
.HasColumnType("timestamp with time zone")
.HasColumnName("start_date");
b.Property<int>("Status")
.HasColumnType("integer")
.HasColumnName("status_id");
b.Property<string>("TitleEn")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("title_en");
b.Property<string>("TitleVi")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("character varying(200)")
.HasColumnName("title_vi");
b.Property<int>("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<int>("Id")
.HasColumnType("integer")
.HasColumnName("id");
b.Property<string>("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<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<string>("Metadata")
.HasColumnType("jsonb")
.HasColumnName("metadata");
b.Property<string>("Operator")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("character varying(20)")
.HasColumnName("operator");
b.Property<string>("RuleType")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("rule_type");
b.Property<string>("Value")
.IsRequired()
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasColumnName("value");
b.Property<Guid>("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<int>("Id")
.HasColumnType("integer")
.HasColumnName("id");
b.Property<string>("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<int>("Id")
.HasColumnType("integer")
.HasColumnName("id");
b.Property<string>("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<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTime?>("ClaimedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("claimed_at");
b.Property<DateTime>("EarnedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("earned_at");
b.Property<DateTime?>("ExpiresAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("expires_at");
b.Property<Guid>("SourceId")
.HasColumnType("uuid")
.HasColumnName("source_id");
b.Property<int>("Status")
.HasColumnType("integer")
.HasColumnName("status");
b.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("type");
b.Property<Guid>("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<int>("Id")
.HasColumnType("integer")
.HasColumnName("id");
b.Property<string>("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<Guid>("Id")
.HasColumnType("uuid")
.HasColumnName("id");
b.Property<DateTime?>("ClaimedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("claimed_at");
b.Property<DateTime?>("CompletedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("completed_at");
b.Property<Guid>("MissionId")
.HasColumnType("uuid")
.HasColumnName("mission_id");
b.Property<bool>("RewardClaimed")
.HasColumnType("boolean")
.HasColumnName("reward_claimed");
b.Property<DateTime>("StartedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("started_at");
b.Property<int>("Status")
.HasColumnType("integer")
.HasColumnName("status_id");
b.Property<Guid>("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<Guid>("MissionId")
.HasColumnType("uuid");
b1.Property<string>("BadgeId")
.HasMaxLength(50)
.HasColumnType("character varying(50)")
.HasColumnName("reward_badge_id");
b1.Property<int>("ExperiencePoints")
.HasColumnType("integer")
.HasColumnName("reward_xp");
b1.Property<decimal>("MiningBoostPercent")
.HasPrecision(5, 2)
.HasColumnType("numeric(5,2)")
.HasColumnName("reward_mining_boost");
b1.Property<decimal>("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<Guid>("UserRewardId")
.HasColumnType("uuid");
b1.Property<decimal>("BonusPoints")
.HasPrecision(18, 2)
.HasColumnType("numeric(18,2)")
.HasColumnName("bonus_points");
b1.Property<string>("Currency")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(10)
.HasColumnType("character varying(10)")
.HasDefaultValue("MP")
.HasColumnName("currency");
b1.Property<decimal>("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<Guid>("UserTaskId")
.HasColumnType("uuid");
b1.Property<DateTime>("CapturedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("evidence_captured_at");
b1.Property<string>("Data")
.IsRequired()
.HasMaxLength(4000)
.HasColumnType("character varying(4000)")
.HasColumnName("evidence_data");
b1.Property<string>("ScreenshotUrl")
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasColumnName("evidence_screenshot_url");
b1.Property<int>("Type")
.HasColumnType("integer")
.HasColumnName("evidence_type");
b1.Property<string>("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<Guid>("UserTaskId")
.HasColumnType("uuid");
b1.Property<int>("CurrentValue")
.HasColumnType("integer")
.HasColumnName("progress_current");
b1.Property<DateTime>("LastUpdated")
.HasColumnType("timestamp with time zone")
.HasColumnName("progress_updated_at");
b1.Property<int>("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<Guid>("UserTaskId")
.HasColumnType("uuid");
b1.Property<string>("FailureReason")
.HasMaxLength(500)
.HasColumnType("character varying(500)")
.HasColumnName("verification_failure_reason");
b1.Property<bool>("IsValid")
.HasColumnType("boolean")
.HasColumnName("verification_valid");
b1.Property<int>("Method")
.HasColumnType("integer")
.HasColumnName("verification_method");
b1.Property<DateTime>("VerifiedAt")
.HasColumnType("timestamp with time zone")
.HasColumnName("verification_at");
b1.Property<Guid?>("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
}
}
}