# MissionService - Service Documentation

## 1. Overview

**Purpose**: Gamification microservice that manages missions (tasks users complete for rewards), daily check-in streaks with tiered bonus points, and reward tracking. Supports mission types including video watching, link clicking, content uploading, friend invitations, daily check-ins, and social actions.

**Port**: 5000 (development, via launchSettings.json), 8080 (Docker/production)

**Database**: PostgreSQL (`mission_service` default in appsettings.json, configurable via `ConnectionStrings:DefaultConnection` or `DATABASE_URL`)

**Framework**: .NET 10.0, C# 14, Clean Architecture + CQRS

**Solution file**: `MissionService.slnx`

**Migration**: `20260117134348_InitialCreate` — auto-applied on startup via `dbContext.Database.MigrateAsync()`

---

## 2. API Endpoints

### MissionsController — `api/v1/missions` (Authorize)

| Method | Route | Auth | Description |
|--------|-------|------|-------------|
| GET | `/api/v1/missions` | JWT required | Get all available missions for the authenticated user. Returns `MissionsListResult`. |
| GET | `/api/v1/missions/category/{category}` | AllowAnonymous | Get missions filtered by category name (e.g., "Daily", "Weekly"). Returns `MissionsListResult`. |
| GET | `/api/v1/missions/{id:guid}` | AllowAnonymous | Get mission details by ID. Returns `MissionDetailsResult` or 404. Optionally includes user progress if authenticated. |
| POST | `/api/v1/missions/{id:guid}/start` | JWT required | Start a mission task for the user. Returns `StartTaskResult`. |
| PUT | `/api/v1/missions/tasks/{taskId:guid}/progress` | JWT required | Update task progress. Body: `UpdateProgressRequest(CurrentValue, Evidence?)`. Returns `UpdateProgressResult`. |
| POST | `/api/v1/missions/tasks/{taskId:guid}/claim` | JWT required | Claim reward for a completed task. Returns `ClaimRewardResult`. |

### CheckInsController — `api/v1/checkins` (Authorize)

| Method | Route | Auth | Description |
|--------|-------|------|-------------|
| POST | `/api/v1/checkins` | JWT required | Perform daily check-in. Returns `CheckInResult` with streak info and points earned. |
| GET | `/api/v1/checkins/status` | JWT required | Get current check-in status (streak, total, can check-in today, next reward preview). |
| GET | `/api/v1/checkins/history?year=&month=` | JWT required | Get check-in history for a specific month. Defaults to current month. |
| GET | `/api/v1/checkins/leaderboard?count=10` | AllowAnonymous | Get check-in streak leaderboard (max 100 entries). |
| GET | `/api/v1/checkins/config` | AllowAnonymous | Get streak bonus tier configuration for client display. |

### AdminController — `api/v1/admin` (Authorize Roles="Admin")

| Method | Route | Description |
|--------|-------|-------------|
| GET | `/api/v1/admin/checkins/users/{userId:guid}` | Get a user's check-in profile details. |
| POST | `/api/v1/admin/checkins/users/{userId:guid}/reset-streak` | Reset a user's current streak to 0. |
| GET | `/api/v1/admin/checkins/top-streaks?count=20` | Get top streak users (max 100). |
| GET | `/api/v1/admin/tasks/pending-verification` | Get all tasks pending admin verification. |
| POST | `/api/v1/admin/tasks/{taskId:guid}/approve` | Approve a pending verification task. |
| POST | `/api/v1/admin/tasks/{taskId:guid}/reject` | Reject a pending verification task. Body: `RejectTaskRequest(Reason)`. |
| GET | `/api/v1/admin/tasks/users/{userId:guid}` | Get all tasks for a specific user. |

### AdminMissionsController — `api/v1/admin/missions` (Authorize Roles="Admin")

| Method | Route | Description |
|--------|-------|-------------|
| GET | `/api/v1/admin/missions` | Get all active missions (admin view with full details). |
| POST | `/api/v1/admin/missions` | Create a new mission. Body: `CreateMissionRequest`. Returns 201. |
| GET | `/api/v1/admin/missions/{id:guid}` | Get mission by ID (admin view). |
| POST | `/api/v1/admin/missions/{id:guid}/activate` | Activate a draft or paused mission. |
| POST | `/api/v1/admin/missions/{id:guid}/pause` | Pause an active mission. |
| POST | `/api/v1/admin/missions/{id:guid}/archive` | Archive a mission (any status). |

### Health Endpoints

| Route | Description |
|-------|-------------|
| `/health` | Full health check (includes PostgreSQL). |
| `/health/live` | Liveness probe (app is running). |
| `/health/ready` | Readiness probe (includes PostgreSQL). |

---

## 3. Commands

### PerformCheckInCommand
- **File**: `Application/Commands/CheckInCommands.cs`
- **Parameters**: `Guid UserId`
- **Result**: `CheckInResult(Success, CurrentStreak, DailyPoints, StreakBonus, MilestoneBonus, TotalPoints, IsMilestone, Message)`
- **Behavior**: Gets or creates user's check-in profile, validates not already checked in today, calculates streak continuation or reset, applies tiered bonus config, saves CheckInDay record.

### StartMissionTaskCommand
- **File**: `Application/Commands/TaskCommands.cs`
- **Parameters**: `Guid UserId, Guid MissionId`
- **Result**: `StartTaskResult(Success, TaskId?, Message)`
- **Behavior**: Validates mission exists and is available, checks no existing active task for this user+mission, checks max completions not reached, creates new `UserTask`.

### UpdateTaskProgressCommand
- **File**: `Application/Commands/TaskCommands.cs`
- **Parameters**: `Guid UserId, Guid TaskId, int CurrentValue, TaskEvidenceDto? Evidence`
- **Result**: `UpdateProgressResult(Success, CurrentValue, TargetValue, PercentComplete, IsComplete, Message)`
- **Behavior**: Validates task ownership and InProgress status, updates progress value, auto-submits evidence if provided and complete, auto-completes if no verification needed.

### ClaimTaskRewardCommand
- **File**: `Application/Commands/TaskCommands.cs`
- **Parameters**: `Guid UserId, Guid TaskId`
- **Result**: `ClaimRewardResult(Success, PointsEarned?, Message)`
- **Behavior**: Validates task ownership, completed status, and not already claimed. Looks up mission reward points, marks task as claimed.

---

## 4. Queries

### GetAvailableMissionsQuery
- **Parameters**: `Guid UserId`
- **Returns**: `MissionsListResult` — list of active missions with optional user task progress per mission.

### GetMissionDetailsQuery
- **Parameters**: `Guid MissionId, Guid? UserId`
- **Returns**: `MissionDetailsResult?` — full mission details (bilingual titles, reward, rules, frequency) with optional user progress. Returns null if not found.

### GetMissionsByCategoryQuery
- **Parameters**: `string Category` (display name, e.g., "Daily")
- **Returns**: `MissionsListResult` — active missions filtered by category. No user progress included.

### GetUserMissionProgressQuery
- **Parameters**: `Guid UserId`
- **Returns**: `UserMissionProgressResult` — aggregated stats (total, completed, in-progress counts, total points, active/completed mission lists).
- **Note**: Query record is defined but no handler implementation exists in the codebase.

### GetCheckInStatusQuery
- **Parameters**: `Guid UserId`
- **Returns**: `CheckInStatusResult(CurrentStreak, LongestStreak, TotalCheckIns, LastCheckInDate, CanCheckInToday, NextReward)` — includes a preview of the next reward milestone.

### GetCheckInHistoryQuery
- **Parameters**: `Guid UserId, int Year, int Month`
- **Returns**: `CheckInHistoryResult(Year, Month, Days[])` — list of `CheckInDayDto(Date, PointsEarned, IsMilestone, StreakOnDay)` for the requested month.

### GetCheckInLeaderboardQuery
- **Parameters**: `int Count` (default 10)
- **Returns**: `CheckInLeaderboardResult(Entries[])` — ranked list by longest streak, then total check-ins.

---

## 5. Domain Model

### Aggregates

#### Mission (Aggregate Root)
- **File**: `Domain/AggregatesModel/MissionAggregate/Mission.cs`
- **Properties**: Code (unique), TitleEn, TitleVi, DescriptionEn, DescriptionVi, Type (MissionType), Category (MissionCategory), Reward (MissionReward), Frequency (FrequencyType), MaxCompletions, StartDate, EndDate, Status (MissionStatus), Priority, Rules (collection of MissionRule)
- **Behavior methods**: `Activate()` (Draft/Paused -> Active), `Pause()` (Active -> Paused), `Archive()` (any -> Archived), `AddRule(MissionRule)`, `IsAvailable()`, `UpdateDetails(...)`
- **State machine**: Draft -> Active <-> Paused -> Archived; any -> Archived

#### UserCheckIn (Aggregate Root)
- **File**: `Domain/AggregatesModel/CheckInAggregate/UserCheckIn.cs`
- **Properties**: UserId (unique), CurrentStreak, LongestStreak, TotalCheckIns, LastCheckInDate (DateOnly?), CheckInDays (collection of CheckInDay)
- **Behavior methods**: `CanCheckInToday()`, `CheckIn(StreakBonusConfig)` — calculates streak continuation/reset and returns points, `GetMonthlyCheckIns(year, month)`, `ResetStreak()` (admin)

#### UserTask (Aggregate Root)
- **File**: `Domain/AggregatesModel/TaskAggregate/UserTask.cs`
- **Properties**: UserId, MissionId, Status (TaskStatus), Progress (TaskProgress), Evidence (TaskEvidence?), Verification (VerificationResult?), RewardClaimed, StartedAt, CompletedAt, ClaimedAt
- **Behavior methods**: `UpdateProgress(int)`, `SubmitEvidence(TaskEvidence)`, `Complete(VerificationResult)`, `AutoComplete()`, `ClaimReward()`, `Cancel()`
- **State machine**: InProgress -> PendingVerification -> Completed/Rejected; InProgress -> Completed (auto); any (unclaimed) -> Cancelled

#### UserReward (Aggregate Root)
- **File**: `Domain/AggregatesModel/RewardAggregate/UserReward.cs`
- **Properties**: UserId, SourceId (TaskId or CheckInId), Type (RewardType enum), Status (RewardStatus enum), Amount (RewardAmount), EarnedAt, ClaimedAt, ExpiresAt
- **Factory methods**: `ForMission(...)`, `ForCheckIn(...)`, `ForMilestone(...)`, `ForReferral(...)`
- **Behavior methods**: `Claim()`, `Expire()`, `Cancel(string reason)`

### Entities (non-root)

#### MissionRule
- **Properties**: RuleType, Operator, Value, Metadata (jsonb)
- **Factory methods**: `MinDuration(seconds)`, `MinWatchPercent(percent)`, `SpecificUrl(url)`
- **Owned by**: Mission (cascade delete)

#### CheckInDay
- **Properties**: Date (DateOnly), PointsEarned, IsMilestone, StreakOnDay
- **Owned by**: UserCheckIn (cascade delete)

### Value Objects

#### MissionReward
- **Properties**: Points (decimal), MiningBoostPercent (decimal), ExperiencePoints (int), BadgeId (string?)
- **Factory**: `Create(points, miningBoost, xp, badgeId)`, `PointsOnly(points)`

#### TaskProgress
- **Properties**: CurrentValue (int), TargetValue (int), LastUpdated (DateTime)
- **Computed**: PercentComplete, IsComplete
- **Methods**: `WithValue(int)`, `Increment(int)`

#### TaskEvidence
- **Properties**: Type (EvidenceType enum), Data, ScreenshotUrl, VideoUrl, CapturedAt
- **Factory methods**: `WatchDuration(seconds)`, `Click(url)`, `Upload(fileUrl, isImage)`, `SocialProof(screenshotUrl)`, `InviteCode(code)`

#### VerificationResult
- **Properties**: IsValid, FailureReason, Method (VerificationMethod enum), VerifiedBy (Guid?), VerifiedAt
- **Factory methods**: `AutoApproved()`, `AiApproved()`, `ManualApproved(adminId)`, `Rejected(reason, method, adminId?)`

#### RewardAmount
- **Properties**: Points (decimal), BonusPoints (decimal), Currency (string, default "MP")
- **Computed**: TotalPoints
- **Methods**: `WithBonus(additionalBonus)`

#### StreakBonusConfig
- **Properties**: BasePoints (decimal), Tiers (list of StreakTier)
- **Default tiers**: Days 1-6: 2 MP/day; Day 7: 3 MP + 20 MP milestone; Days 8-13: 3 MP; Day 14: 4 MP + 35 MP milestone; Days 15-20: 4 MP; Day 21: 5 MP + 50 MP milestone; Days 22-29: 5 MP; Day 30+: 10 MP + 100 MP milestone
- **Method**: `CalculateReward(streakDay)` returns (DailyPoints, StreakBonus, MilestoneBonus, IsMilestone)

### Enumerations (Type-safe enum pattern)

#### MissionType
| Id | Name | Description |
|----|------|-------------|
| 1 | Video | Watch video to earn rewards |
| 2 | Click | Click on links/ads |
| 3 | Upload | Upload user-generated content |
| 4 | Invite | Invite friends |
| 5 | CheckIn | Daily check-in |
| 6 | Social | Social actions (like, share, subscribe) |

#### MissionCategory
| Id | Name |
|----|------|
| 1 | Daily |
| 2 | Weekly |
| 3 | Special |
| 4 | Onboarding |
| 5 | Event |

#### MissionStatus
| Id | Name |
|----|------|
| 1 | Draft |
| 2 | Active |
| 3 | Paused |
| 4 | Expired |
| 5 | Archived |

#### FrequencyType
| Id | Name | Description |
|----|------|-------------|
| 1 | Once | Complete once total |
| 2 | Daily | Once per day |
| 3 | Weekly | Once per week |
| 4 | Unlimited | Unlimited times |

#### TaskStatus
| Id | Name |
|----|------|
| 1 | Pending |
| 2 | InProgress |
| 3 | PendingVerification |
| 4 | Completed |
| 5 | Rejected |
| 6 | Cancelled |
| 7 | Expired |

### Enums (C# enum)

#### EvidenceType
`WatchDuration = 1, ClickData = 2, UploadedContent = 3, SocialProof = 4, InviteCode = 5`

#### VerificationMethod
`Automatic = 1, AI = 2, Manual = 3`

#### RewardType
`MissionComplete = 1, CheckInDaily = 2, CheckInMilestone = 3, ReferralBonus = 4, SocialAction = 5`

#### RewardStatus
`Pending = 1, Claimed = 2, Expired = 3, Cancelled = 4`

---

## 6. Database Schema

Database: PostgreSQL. All column names use snake_case. Migration: `20260117134348_InitialCreate`.

### Table: `missions`
| Column | Type | Constraints |
|--------|------|-------------|
| id | uuid | PK, not generated |
| code | varchar(100) | NOT NULL, UNIQUE INDEX |
| title_en | varchar(200) | NOT NULL |
| title_vi | varchar(200) | NOT NULL |
| description_en | varchar(2000) | nullable |
| description_vi | varchar(2000) | nullable |
| type_id | integer | NOT NULL (FK concept to mission_types) |
| category_id | integer | NOT NULL (FK concept to mission_categories) |
| reward_points | numeric(18,2) | NOT NULL (owned: MissionReward) |
| reward_mining_boost | numeric(5,2) | NOT NULL (owned: MissionReward) |
| reward_xp | integer | NOT NULL (owned: MissionReward) |
| reward_badge_id | varchar(50) | nullable (owned: MissionReward) |
| frequency_id | integer | NOT NULL (FK concept to frequency_types) |
| max_completions | integer | NOT NULL |
| start_date | timestamp with time zone | NOT NULL |
| end_date | timestamp with time zone | nullable |
| status_id | integer | NOT NULL (FK concept to mission_statuses) |
| priority | integer | NOT NULL |

**Indexes**: `IX_missions_code` (unique on code)

### Table: `mission_rules`
| Column | Type | Constraints |
|--------|------|-------------|
| id | uuid | PK, not generated |
| mission_id | uuid | NOT NULL, FK -> missions(id) CASCADE |
| rule_type | varchar(50) | NOT NULL |
| operator | varchar(20) | NOT NULL |
| value | varchar(500) | NOT NULL |
| metadata | jsonb | nullable |

**Indexes**: `IX_mission_rules_mission_id`

### Table: `user_tasks`
| Column | Type | Constraints |
|--------|------|-------------|
| id | uuid | PK, not generated |
| user_id | uuid | NOT NULL |
| mission_id | uuid | NOT NULL |
| status_id | integer | NOT NULL |
| progress_current | integer | NOT NULL (owned: TaskProgress) |
| progress_target | integer | NOT NULL (owned: TaskProgress) |
| progress_updated_at | timestamp with time zone | NOT NULL (owned: TaskProgress) |
| evidence_type | integer | nullable (owned: TaskEvidence) |
| evidence_data | varchar(4000) | nullable (owned: TaskEvidence) |
| evidence_screenshot_url | varchar(500) | nullable (owned: TaskEvidence) |
| evidence_video_url | varchar(500) | nullable (owned: TaskEvidence) |
| evidence_captured_at | timestamp with time zone | nullable (owned: TaskEvidence) |
| verification_valid | boolean | nullable (owned: VerificationResult) |
| verification_failure_reason | varchar(500) | nullable (owned: VerificationResult) |
| verification_method | integer | nullable (owned: VerificationResult) |
| verification_by | uuid | nullable (owned: VerificationResult) |
| verification_at | timestamp with time zone | nullable (owned: VerificationResult) |
| reward_claimed | boolean | NOT NULL |
| started_at | timestamp with time zone | NOT NULL |
| completed_at | timestamp with time zone | nullable |
| claimed_at | timestamp with time zone | nullable |

**Indexes**: `IX_user_tasks_user_id`, `IX_user_tasks_mission_id`, `IX_user_tasks_user_id_mission_id` (composite)

### Table: `user_checkins`
| Column | Type | Constraints |
|--------|------|-------------|
| id | uuid | PK, not generated |
| user_id | uuid | NOT NULL, UNIQUE INDEX |
| current_streak | integer | NOT NULL |
| longest_streak | integer | NOT NULL |
| total_checkins | integer | NOT NULL |
| last_checkin_date | date | nullable |

**Indexes**: `IX_user_checkins_user_id` (unique)

### Table: `checkin_days`
| Column | Type | Constraints |
|--------|------|-------------|
| id | uuid | PK, not generated |
| user_checkin_id | uuid | NOT NULL, FK -> user_checkins(id) CASCADE |
| date | date | NOT NULL |
| points_earned | numeric(18,2) | NOT NULL |
| is_milestone | boolean | NOT NULL |
| streak_on_day | integer | NOT NULL |

**Indexes**: `IX_checkin_days_user_checkin_id_date` (unique composite)

### Table: `user_rewards`
| Column | Type | Constraints |
|--------|------|-------------|
| id | uuid | PK, not generated |
| user_id | uuid | NOT NULL |
| source_id | uuid | NOT NULL, UNIQUE INDEX |
| type | integer | NOT NULL (RewardType enum) |
| status | integer | NOT NULL (RewardStatus enum) |
| points | numeric(18,2) | NOT NULL (owned: RewardAmount) |
| bonus_points | numeric(18,2) | NOT NULL (owned: RewardAmount) |
| currency | varchar(10) | NOT NULL, default "MP" (owned: RewardAmount) |
| earned_at | timestamp with time zone | NOT NULL |
| claimed_at | timestamp with time zone | nullable |
| expires_at | timestamp with time zone | nullable |

**Indexes**: `IX_user_rewards_user_id`, `IX_user_rewards_source_id` (unique), `IX_user_rewards_status_expires_at` (composite)

### Lookup Tables (seeded)

| Table | Columns | Seed Data |
|-------|---------|-----------|
| `mission_types` | id (int PK), name (varchar 50) | 1=Video, 2=Click, 3=Upload, 4=Invite, 5=CheckIn, 6=Social |
| `mission_categories` | id (int PK), name (varchar 50) | 1=Daily, 2=Weekly, 3=Special, 4=Onboarding, 5=Event |
| `mission_statuses` | id (int PK), name (varchar 50) | 1=Draft, 2=Active, 3=Paused, 4=Expired, 5=Archived |
| `frequency_types` | id (int PK), name (varchar 50) | 1=Once, 2=Daily, 3=Weekly, 4=Unlimited |
| `task_statuses` | id (int PK), name (varchar 50) | 1=Pending, 2=InProgress, 3=PendingVerification, 4=Completed, 5=Rejected, 6=Cancelled, 7=Expired |

---

## 7. Integration Events

**No integration events are currently implemented.** The domain supports domain events via the `Entity.AddDomainEvent()` / `MissionDbContext.DispatchDomainEventsAsync()` infrastructure, but no domain event records or handlers exist in the codebase. No RabbitMQ integration event publishers or consumers are present.

The `Domain/Events/` directory referenced in the template pattern does not exist in this service.

---

## 8. Dependencies

### NuGet Packages

**API Layer** (`MissionService.API.csproj`):
| Package | Version |
|---------|---------|
| MediatR | 12.4.1 |
| FluentValidation | 11.11.0 |
| FluentValidation.DependencyInjectionExtensions | 11.11.0 |
| Microsoft.EntityFrameworkCore.Design | 10.0.2 |
| Swashbuckle.AspNetCore | 7.2.0 |
| Asp.Versioning.Mvc | 8.1.0 |
| Asp.Versioning.Mvc.ApiExplorer | 8.1.0 |
| AspNetCore.HealthChecks.NpgSql | 8.0.2 |
| AspNetCore.HealthChecks.Redis | 8.0.1 |
| Hellang.Middleware.ProblemDetails | 6.5.1 |
| Serilog.AspNetCore | 8.0.3 |
| Serilog.Sinks.Console | 6.0.0 |
| Serilog.Sinks.Seq | 8.0.0 |
| Microsoft.AspNetCore.Authentication.JwtBearer | 9.0.0 |

**Domain Layer** (`MissionService.Domain.csproj`):
| Package | Version |
|---------|---------|
| MediatR.Contracts | 2.0.1 |

**Infrastructure Layer** (`MissionService.Infrastructure.csproj`):
| Package | Version |
|---------|---------|
| Microsoft.EntityFrameworkCore | 10.0.0 |
| Npgsql.EntityFrameworkCore.PostgreSQL | 10.0.0 |
| Microsoft.EntityFrameworkCore.Tools | 10.0.0 |
| MediatR | 12.4.1 |
| Dapper | 2.1.35 |
| Microsoft.Extensions.Http.Polly | 9.0.0 |
| Polly | 8.5.0 |
| StackExchange.Redis | 2.8.16 |

**Shared** (`Directory.Build.props`):
| Package | Version |
|---------|---------|
| Microsoft.SourceLink.GitHub | 8.0.0 |

### External Service Dependencies
- **PostgreSQL**: Primary database (connection via Npgsql)
- **IAM Service**: JWT token validation (Authority: `http://iam-service-net:8080` default)
- **Redis**: Package referenced but not actively used in application code (health check configured)

---

## 9. Configuration

### Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `ASPNETCORE_ENVIRONMENT` | `Production` (Docker) / `Development` (launch) | Runtime environment |
| `ASPNETCORE_URLS` | `http://+:8080` (Docker) / `http://localhost:5000` (launch) | Listening URLs |
| `DATABASE_URL` | — | Fallback connection string if `ConnectionStrings:DefaultConnection` not set |

### appsettings.json Keys

| Key | Default Value | Description |
|-----|---------------|-------------|
| `ConnectionStrings:DefaultConnection` | `Host=localhost;Port=5432;Database=mission_service;Username=postgres;Password=postgres` | PostgreSQL connection string |
| `Redis:ConnectionString` | `localhost:6379` | Redis connection string |
| `Jwt:Secret` | `your-super-secret-key-min-32-characters` | JWT signing key |
| `Jwt:Issuer` | `goodgo-platform` | JWT issuer |
| `Jwt:Audience` | `goodgo-services` | JWT audience |
| `Jwt:AccessTokenExpiryMinutes` | `15` | Access token lifetime |
| `Jwt:RefreshTokenExpiryDays` | `7` | Refresh token lifetime |
| `Jwt:Authority` | `http://iam-service-net:8080` | OIDC authority for token validation |

### MediatR Pipeline (execution order)

1. `LoggingBehavior` — Logs request name and elapsed time (Stopwatch)
2. `ValidatorBehavior` — Runs FluentValidation validators, throws `ValidationException` on failure
3. `TransactionBehavior` — Wraps commands in a database transaction (skips queries ending in "Query"), uses `ExecutionStrategy` for retry

### Docker

- Multi-stage build: `sdk:10.0` (build) -> `aspnet:10.0` (runtime)
- Non-root user: `dotnetuser` (UID/GID 1001)
- Health check: `curl -f http://localhost:8080/health/live` (30s interval, 3 retries, 10s start period)
- Exposed port: 8080

### Build Properties

- Target: `net10.0`, C# `14.0`
- Nullable reference types: enabled
- Implicit usings: enabled
- Treat warnings as errors: true
- XML documentation generation: enabled

---

## 10. Tests

### Unit Tests (`tests/MissionService.UnitTests/`)
- `Application/Commands/PerformCheckInCommandHandlerTests.cs` — Tests for the check-in command handler
- `Domain/MissionAggregateTests.cs` — Tests for Mission entity behavior
- `Domain/UserCheckInAggregateTests.cs` — Tests for UserCheckIn streak logic

### Functional Tests (`tests/MissionService.FunctionalTests/`)
- `CustomWebApplicationFactory.cs` — Test server factory (swaps DbContext)
- `Controllers/MissionsControllerTests.cs` — API endpoint integration tests

---

## 11. Notable Gaps / Observations

1. **GetUserMissionProgressQuery**: Query record is defined but has no handler implementation.
2. **No FluentValidation validators exist**: The `ValidatorBehavior` is registered but no `AbstractValidator<T>` classes are present in the codebase.
3. **No domain events defined**: The infrastructure for dispatching domain events exists in `MissionDbContext`, but no domain event records (`INotification`) are defined or raised by any entity.
4. **No integration events**: No RabbitMQ publishers/consumers. Reward claiming does not notify external services (e.g., wallet service).
5. **UserReward aggregate**: Has a full repository and EF configuration but is not used by any command handler — rewards are tracked via `UserTask.RewardClaimed` flag only.
6. **Idempotency**: `IRequestManager`/`RequestManager` are registered but not used by any handler or behavior.
7. **Redis**: Package referenced and health check configured, but no caching logic exists in the codebase.
8. **Database name**: Default in appsettings is `mission_service` (renamed from template default).
9. **JWT audience validation**: Disabled (`ValidateAudience = false`, `ValidateIssuer = false`) — relies on IAM service for token signing.
