Files
pos-system/services/mining-service-net/SERVICE_DOCS.md
Ho Ngoc Hai f3779c4ebe docs: add SERVICE_DOCS.md for all 24 microservices from per-service code audit
Each SERVICE_DOCS.md documents: Overview, API Endpoints, Commands, Queries,
Domain Model, Database Schema, Integration Events, Dependencies, Configuration.
Generated by 23 parallel audit agents reading actual source code.

Key corrections from audit:
- inventory-service: 12 commands/6 queries (was listed as scaffold)
- promotion-service: 12 commands/10 queries (was listed as 0)
- mission-service: 4 commands/7 queries (was listed as 0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:54:53 +07:00

560 lines
30 KiB
Markdown

# MiningService - Service Documentation
## 1. Overview
**Purpose**: Pi Network-style point mining service. Users ("miners") earn Mining Points (MP) over time through 24-hour mining sessions. The service manages miner profiles, mining sessions with streak bonuses, security circles (trusted groups for bonus multipliers), and referral programs.
**Port**: `5006` (Development, via launchSettings.json) / `8080` (Docker/Production)
**Database**: PostgreSQL -- `mining_service` on Neon PostgreSQL (`ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech`)
**Solution**: `MiningService.slnx`
**Target Framework**: .NET 10.0, C# 14
**Architecture**: Clean Architecture + CQRS (MediatR)
**Real-time**: SignalR Hub at `/hubs/mining` for live mining updates
**Auth**: JWT Bearer (Duende IdentityServer), with SignalR token via query string
---
## 2. API Endpoints
### MiningController (`/api/v1/mining`) -- [Authorize]
| Method | Route | Description | Request | Response |
|--------|-------|-------------|---------|----------|
| GET | `/api/v1/mining/me?userId={guid}` | Get current miner status | Query: `userId` (Guid) | `MinerStatusDto` or 404 |
| POST | `/api/v1/mining/start` | Start a new 24h mining session | Body: `StartMiningRequest { UserId }` | `StartMiningResult` |
| POST | `/api/v1/mining/claim` | Claim mining reward from completed session | Body: `ClaimMiningRequest { UserId }` | `ClaimMiningRewardResult` |
| GET | `/api/v1/mining/history?userId={guid}&page=1&pageSize=20` | Get mining history (paginated) | Query params | `MiningHistoryDto` |
| GET | `/api/v1/mining/rate?userId={guid}` | Get current mining rate breakdown | Query: `userId` (Guid) | `MiningRateDto` or 404 |
| GET | `/api/v1/mining/leaderboard?limit=100` | Get top miners leaderboard | Query: `limit` (int) | `LeaderboardDto` -- **[AllowAnonymous]** |
### CirclesController (`/api/v1/circles`) -- [Authorize]
| Method | Route | Description | Request | Response |
|--------|-------|-------------|---------|----------|
| GET | `/api/v1/circles/me?userId={guid}` | Get user's circle | Query: `userId` (Guid) | `CircleDto` or 404 |
| POST | `/api/v1/circles` | Create a new security circle | Body: `CreateCircleRequest { UserId, Name }` | `CreateCircleResult` (201) |
| POST | `/api/v1/circles/invite` | Invite a member to circle | Body: `InviteMemberRequest { UserId, TargetMinerId }` | 200 message |
| POST | `/api/v1/circles/accept/{inviteId}?userId={guid}` | Accept circle invitation | Path: `inviteId`, Query: `userId` | `AcceptCircleInviteResult` |
| DELETE | `/api/v1/circles/members/{memberId}?ownerId={guid}` | Remove member from circle | Path: `memberId`, Query: `ownerId` | `RemoveCircleMemberResult` |
| GET | `/api/v1/circles/trust-score?userId={guid}` | Get circle trust score | Query: `userId` (Guid) | `CircleTrustScoreDto` or 404 |
### ReferralsController (`/api/v1/referrals`) -- [Authorize]
| Method | Route | Description | Request | Response |
|--------|-------|-------------|---------|----------|
| GET | `/api/v1/referrals?userId={guid}` | Get referral code and referrals list | Query: `userId` (Guid) | `ReferralsDto` |
| POST | `/api/v1/referrals/apply` | Apply a referral code | Body: `ApplyReferralRequest { UserId, ReferralCode }` | `ApplyReferralResult` |
| GET | `/api/v1/referrals/code?userId={guid}` | Get my referral code | Query: `userId` (Guid) | `ReferralCodeDto` or 404 |
| GET | `/api/v1/referrals/stats?userId={guid}` | Get referral statistics | Query: `userId` (Guid) | `ReferralStatsDto` or 404 |
### AdminController (`/api/v1/admin`) -- [Authorize(Roles = "Admin")]
#### Configuration
| Method | Route | Description | Request | Response |
|--------|-------|-------------|---------|----------|
| GET | `/api/v1/admin/config` | Get all system configuration | -- | `SystemConfigDto` |
| PUT | `/api/v1/admin/config` | Update system configuration | Body: `UpdateSystemConfigCommand` | `UpdateConfigResult` |
| GET | `/api/v1/admin/config/mining` | Get mining configuration | -- | `MiningConfigDto` |
| PUT | `/api/v1/admin/config/mining` | Update mining configuration | Body: `UpdateMiningConfigCommand` | `UpdateConfigResult` |
| GET | `/api/v1/admin/config/streak` | Get streak configuration | -- | `StreakConfigDto` |
| PUT | `/api/v1/admin/config/streak` | Update streak configuration | Body: `UpdateStreakConfigCommand` | `UpdateConfigResult` |
| GET | `/api/v1/admin/config/referral` | Get referral configuration | -- | `ReferralConfigDto` |
| PUT | `/api/v1/admin/config/referral` | Update referral configuration | Body: `UpdateReferralConfigCommand` | `UpdateConfigResult` |
#### Miner Management
| Method | Route | Description | Request | Response |
|--------|-------|-------------|---------|----------|
| GET | `/api/v1/admin/miners?page=1&pageSize=20&search=` | List all miners (paginated) | Query params | `MinersListDto` |
| GET | `/api/v1/admin/miners/{id}` | Get miner details | Path: `id` (Guid) | `MinerDetailsDto` or 404 |
| PUT | `/api/v1/admin/miners/{minerId}/suspend` | Suspend a miner account | Body: `SuspendRequest { Reason }` | 200 message |
| PUT | `/api/v1/admin/miners/{minerId}/ban` | Ban a miner account | Body: `BanRequest { Reason }` | `BanMinerResult` |
| PUT | `/api/v1/admin/miners/{minerId}/restore` | Restore a suspended miner | -- | 200 message |
| PUT | `/api/v1/admin/miners/{minerId}/adjust-points` | Adjust miner points | Body: `AdjustPointsRequest { Amount, Reason }` | `AdjustPointsResult` |
| PUT | `/api/v1/admin/miners/{minerId}/reset-streak` | Reset miner streak | Body: `ResetStreakRequest { Reason }` | `ResetStreakResult` |
#### Analytics
| Method | Route | Description | Response |
|--------|-------|-------------|----------|
| GET | `/api/v1/admin/analytics/overview` | Admin dashboard overview | `AdminOverviewDto` |
| GET | `/api/v1/admin/analytics/miners` | Miner analytics | `MinerAnalyticsDto` |
| GET | `/api/v1/admin/analytics/circles` | Circle analytics | `CircleAnalyticsDto` (stub) |
| GET | `/api/v1/admin/analytics/referrals` | Referral analytics | `ReferralAnalyticsDto` (stub) |
| GET | `/api/v1/admin/analytics/points` | Points analytics | `PointsAnalyticsDto` (stub -- no handler) |
| GET | `/api/v1/admin/analytics/streaks` | Streak analytics | `StreakAnalyticsDto` (stub -- no handler) |
| GET | `/api/v1/admin/audit-logs?page=1&pageSize=50` | Audit logs | `AuditLogsDto` (stub -- no handler) |
### Health Endpoints (no auth)
| Route | Description |
|-------|-------------|
| `/health` | Full health check (includes PostgreSQL) |
| `/health/live` | Liveness probe (app is running) |
| `/health/ready` | Readiness probe (ready for traffic) |
### SignalR Hub
| Endpoint | `/hubs/mining` |
|----------|----------------|
| **JoinMinerGroup(minerId)** | Subscribe to personal mining updates |
| **LeaveMinerGroup(minerId)** | Unsubscribe from personal mining updates |
| **JoinLeaderboardGroup()** | Subscribe to leaderboard updates |
Server-to-client messages via `IMiningHubService`:
- `PointsUpdated` -- `{ earnedPoints, totalPoints, streakDays }`
- `SessionStarted` -- `{ endTime, hourlyRate }`
- `StreakMilestone` -- `{ streakDays, bonusPoints }`
---
## 3. Commands
### Core Commands
| Command | Parameters | Result | Handler |
|---------|-----------|--------|---------|
| `StartMiningCommand` | `UserId` (Guid) | `StartMiningResult { SessionId, HourlyRate, EndTime, StreakDays }` | Validates miner active, no existing session. Recalculates rate, creates MiningSession. |
| `ClaimMiningRewardCommand` | `UserId` (Guid) | `ClaimMiningRewardResult { PointsEarned, TotalPoints, StreakDays, StreakBonus }` | Validates session ready to claim. Calculates points, increments streak, adds milestone bonuses, creates MiningHistory entries. |
| `CreateCircleCommand` | `UserId` (Guid), `Name` (string) | `CreateCircleResult { CircleId, Name }` | Validates miner exists, not already in/owning a circle. Creates Circle with owner as first member. |
| `InviteToCircleCommand` | `UserId` (Guid), `TargetMinerId` (Guid) | `bool` | Validates inviter owns a circle, target not in a circle. Adds member, recalculates rates if circle becomes valid. |
| `ApplyReferralCodeCommand` | `UserId` (Guid), `ReferralCode` (string) | `ApplyReferralResult { ReferralId, ReferrerId, IsActive }` | Validates no existing referrer, valid code, not self-referral. Creates inactive Referral (pending KYC). |
### Circle Management Commands
| Command | Parameters | Result | Handler |
|---------|-----------|--------|---------|
| `AcceptCircleInviteCommand` | `UserId` (Guid), `InviteId` (Guid) | `AcceptCircleInviteResult { Success, Message }` | Simplified: adds user as member to circle by InviteId. |
| `RemoveCircleMemberCommand` | `OwnerId` (Guid), `MemberId` (Guid) | `RemoveCircleMemberResult { Success, Message }` | Validates owner owns circle. Calls circle.RemoveMember(). |
### Admin Commands
| Command | Parameters | Result | Handler |
|---------|-----------|--------|---------|
| `SuspendMinerCommand` | `MinerId` (Guid), `Reason` (string) | `bool` | Calls miner.Suspend(). |
| `RestoreMinerCommand` | `MinerId` (Guid) | `bool` | Calls miner.Restore(). |
| `BanMinerCommand` | `MinerId` (Guid), `Reason` (string) | `BanMinerResult { Success, Message }` | Calls miner.Suspend() (note: uses Suspend, not Ban). |
| `AdjustMinerPointsCommand` | `MinerId` (Guid), `Amount` (decimal), `Reason` (string) | `AdjustPointsResult { Success, NewBalance, Message }` | Calls miner.AddBonusPoints(). |
| `ResetMinerStreakCommand` | `MinerId` (Guid), `Reason` (string) | `ResetStreakResult { Success, Message }` | Stub -- saves but does not actually reset streak. |
| `UpdateSystemConfigCommand` | `Mining?`, `Streak?`, `Referral?` | `UpdateConfigResult { Success, Message }` | Stub -- returns success without persisting. |
| `UpdateMiningConfigCommand` | `BaseRate?`, `SessionDurationHours?`, `MaxSessionsPerDay?` | `UpdateConfigResult` | No dedicated handler (uses UpdateSystemConfigCommandHandler pattern). |
| `UpdateStreakConfigCommand` | `Tiers?`, `GracePeriodDays?` | `UpdateConfigResult` | No dedicated handler registered. |
| `UpdateReferralConfigCommand` | `BonusPerReferral?`, `MaxBonusCap?`, `KycRequired?` | `UpdateConfigResult` | Stub handler -- returns success without persisting. |
---
## 4. Queries
| Query | Parameters | Returns |
|-------|-----------|---------|
| `GetMinerStatusQuery` | `UserId` (Guid) | `MinerStatusDto? { MinerId, Role, TotalMinedPoints, HourlyRate, DailyRate, CurrentStreak, LongestStreak, StreakBonus, HasActiveSession, SessionEndTime, Status }` |
| `GetMiningHistoryQuery` | `UserId`, `Page`, `PageSize` | `MiningHistoryDto { Items[], TotalCount, Page, PageSize }` -- in-memory pagination from miner's histories |
| `GetMiningRateQuery` | `UserId` (Guid) | `MiningRateDto? { BaseRate, RoleMultiplier, CircleBonus, ReferralBonus, StreakBonus, TotalRate, HourlyPoints, DailyPoints }` |
| `GetLeaderboardQuery` | `Limit` (int, default 100) | `LeaderboardDto { Entries[] }` -- top miners by TotalMinedPoints |
| `GetCircleQuery` | `UserId` (Guid) | `CircleDto? { CircleId, Name, OwnerId, MemberCount, TrustScore, BonusMultiplier, IsValid, Status, Members[] }` |
| `GetCircleTrustScoreQuery` | `UserId` (Guid) | `CircleTrustScoreDto? { CircleId, Name, MemberCount, TrustScore, IsValid, BonusMultiplier }` |
| `GetReferralsQuery` | `UserId` (Guid) | `ReferralsDto { MyReferralCode, TotalReferrals, ActiveReferrals, TotalBonusPercent, Referrals[] }` |
| `GetReferralCodeQuery` | `UserId` (Guid) | `ReferralCodeDto? { ReferralCode, ShareUrl }` -- URL: `https://goodgo.app/invite/{code}` |
| `GetReferralStatsQuery` | `UserId` (Guid) | `ReferralStatsDto? { ReferralCode, TotalReferrals, ActiveReferrals, PendingReferrals, TotalEarned, CurrentBonusRate }` |
| `GetSystemConfigQuery` | -- | `SystemConfigDto { Mining, Streak, Referral }` -- loads from in-memory ConfigurationRepository |
| `GetReferralConfigQuery` | -- | `ReferralConfigDto { BonusPerReferral, MaxBonusCap, KycRequired }` |
| `GetAdminOverviewQuery` | -- | `AdminOverviewDto { TotalMiners, ActiveMiners, MinersWithActiveSession, TotalPointsMined, TotalCircles, ValidCircles, TotalReferrals, ActiveReferrals }` -- uses DbContext directly |
| `GetMinersListQuery` | `Page`, `PageSize`, `Search?` | `MinersListDto { Items[], TotalCount, Page, PageSize }` |
| `GetMinerDetailsQuery` | `MinerId` (Guid) | `MinerDetailsDto?` -- full miner detail including rate, streak, circle |
| `GetMinerAnalyticsQuery` | -- | `MinerAnalyticsDto { TotalMiners, ActiveMiners, SuspendedMiners, TotalPointsMined, PointsMinedToday, RoleDistribution }` |
| `GetCircleAnalyticsQuery` | -- | `CircleAnalyticsDto` -- **stub**, returns zeros |
| `GetReferralAnalyticsQuery` | -- | `ReferralAnalyticsDto` -- **stub**, returns zeros |
| `GetPointsAnalyticsQuery` | -- | `PointsAnalyticsDto` -- **no handler registered** |
| `GetStreakAnalyticsQuery` | -- | `StreakAnalyticsDto` -- **no handler registered** |
| `GetAuditLogsQuery` | `Page`, `PageSize` | `AuditLogsDto` -- **no handler registered** |
---
## 5. Domain Model
### Aggregates
#### Miner (Aggregate Root)
- **File**: `MiningService.Domain/AggregatesModel/MinerAggregate/Miner.cs`
- **Properties**: `UserId`, `Role` (MinerRole), `TotalMinedPoints`, `CurrentRate` (MiningRate, owned), `ActiveSession` (MiningSession?, owned), `Streak` (MiningStreak, owned), `ReferralCode`, `ReferredById?`, `CircleId?`, `Status` (MinerStatus), `CreatedAt`, `UpdatedAt`, `RowVersion` (concurrency token), `MiningHistories` (collection)
- **Factory**: `Miner.Create(userId, referralCode?, referredById?)` -- generates 8-char alphanumeric referral code, raises `MinerCreatedDomainEvent`
- **Behavior Methods**:
- `StartMiningSession(baseRate, sessionHours)` -- validates active status, no existing session; recalculates rate; creates session; raises `MiningSessionStartedDomainEvent`
- `ClaimMiningReward()` -- validates session ready; calculates points + streak milestone bonuses; increments streak; creates MiningHistory entries; raises `PointsMinedDomainEvent`
- `RecalculateRate(baseRate, circleBonus, referralBonus)` -- computes TotalRate formula
- `UpgradeRole(newRole)` -- only allows upgrades
- `JoinCircle(circleId)` -- auto-upgrades Pioneer to Contributor
- `LeaveCircle()` -- auto-downgrades Contributor to Pioneer
- `Suspend()`, `Restore()`, `Ban()` -- status transitions
- `AddBonusPoints(points, source)`, `DeductPoints(points, reason)` -- with history tracking
**Milestone Bonuses** (on streak claim):
| Streak Day | Bonus MP |
|-----------|----------|
| 7 | 50 |
| 14 | 100 |
| 30 | 300 |
| 60 | 500 |
| 90 | 1000 |
#### Circle (Aggregate Root)
- **File**: `MiningService.Domain/AggregatesModel/CircleAggregate/Circle.cs`
- **Properties**: `OwnerId`, `Name`, `TrustScore` (0-100), `BonusMultiplier`, `Status` (CircleStatus), `CreatedAt`, `UpdatedAt`, `Members` (collection of CircleMember)
- **Constants**: `MinMembers = 3`, `MaxMembers = 5`, `ValidCircleBonus = 0.25m` (25%)
- **Computed**: `ActiveMemberCount`, `IsValid` (Active status + 3+ active members)
- **Factory**: `Circle.Create(ownerId, name)` -- owner auto-added as first member, status = Incomplete
- **Behavior Methods**:
- `AddMember(minerId)` -- validates not disbanded, max 5, not duplicate; recalculates status; raises `CircleCompletedDomainEvent` when reaching 3 members
- `RemoveMember(minerId)` -- cannot remove owner; deactivates member
- `Disband()` -- sets Disbanded, zeroes bonus, deactivates all members
- **Trust Score**: 3 members = 60, 4 = 80, 5 = 100
#### CircleMember (Entity)
- **File**: `MiningService.Domain/AggregatesModel/CircleAggregate/CircleMember.cs`
- **Properties**: `CircleId`, `MinerId`, `JoinedAt`, `IsActive`
- **Methods**: `Deactivate()`, `Activate()`
#### Referral (Aggregate Root)
- **File**: `MiningService.Domain/AggregatesModel/ReferralAggregate/Referral.cs`
- **Properties**: `ReferrerId`, `ReferredId`, `ReferralCode`, `BonusRate` (default 0.25 = 25%), `IsActive`, `Level` (default 1), `CreatedAt`, `ActivatedAt?`
- **Factory**: `Referral.Create(referrerId, referredId, referralCode, bonusRate, level)` -- validates no self-referral, starts inactive
- **Behavior Methods**:
- `Activate()` -- sets active + timestamp; raises `ReferralActivatedDomainEvent`
- `Deactivate()` -- sets inactive
- `CalculateBonus(baseRate)` -- returns `baseRate * BonusRate` if active, else 0
#### MiningHistory (Entity, child of Miner)
- **File**: `MiningService.Domain/AggregatesModel/MinerAggregate/MiningHistory.cs`
- **Properties**: `MinerId`, `PointsEarned`, `Source`, `SessionId?`, `EarnedAt`, `HourlyRateSnapshot`, `StreakDaySnapshot`
- **Factories**: `CreateFromSession(...)`, `CreateFromBonus(...)`
### Configuration Aggregates (in-memory, not persisted to DB)
#### MiningConfiguration
- **Properties**: `BaseRate` (0.25), `SessionDurationHours` (24), `MaxSessionsPerDay` (1), `IsGloballyEnabled` (true), `UpdatedAt`, `UpdatedBy`
#### StreakConfiguration
- **Properties**: `Tiers` (collection of StreakTier), `GracePeriodEnabled` (true), `GracePeriodHours` (24), `RecoveryCost` (50 MP), `FreezeTokenDays` (7), `UpdatedAt`, `UpdatedBy`
- **Default Tiers**:
| MinDays | MaxDays | BonusPercent | BadgeName | MilestoneMpBonus |
|---------|---------|-------------|-----------|-----------------|
| 1 | 2 | 0% | -- | 0 |
| 3 | 6 | 10% | 3-day badge | 0 |
| 7 | 13 | 25% | 7-day badge | 50 |
| 14 | 29 | 50% | 14-day badge | 100 |
| 30 | 59 | 100% | 30-day badge | 300 |
| 60 | 89 | 125% | 60-day badge | 500 |
| 90 | MAX | 150% | 90-day badge | 1000 |
#### ReferralConfiguration
- **Properties**: `BonusPercentPerReferral` (0.25), `MaxBonusPercent` (1.0 = 100%), `KycRequired` (true), `MaxReferralLevels` (1), `UpdatedAt`, `UpdatedBy`
### Value Objects
#### MiningRate (record)
- `BaseRate`, `RoleBonus`, `CircleBonus`, `ReferralBonus`, `StreakBonus`
- `TotalRate = BaseRate * (1 + Role) * (1 + Circle) * (1 + Referral) * (1 + Streak)`
- `DailyRate = TotalRate * 24`
- Default: BaseRate = 0.25 MP/hour
#### MiningStreak (record)
- `CurrentStreak`, `LongestStreak`, `LastMiningDate`, `FreezeTokens`, `IsGracePeriod`
- `BonusMultiplier`: 0% (days 1-2), 10% (3-6), 25% (7-13), 50% (14-29), 100% (30-59), 125% (60-89), 150% (90+)
- `IncrementStreak()` -- earns 1 freeze token per 7 days
- `Reset()` -- zeroes CurrentStreak
#### MiningSession (record)
- `SessionId`, `StartTime`, `EndTime`, `HourlyRate`, `Status` (MiningSessionStatus), `AccumulatedPoints`
- `IsReadyToClaim` -- Active status and EndTime passed
- `CalculateEarnedPoints()` -- elapsed hours * HourlyRate, capped at 24h
- `MarkAsClaimed(earnedPoints)` -- sets Claimed status
### Enumerations
#### MinerRole
| Value | Name | Bonus |
|-------|------|-------|
| 0 | Pioneer | 0% (base user) |
| 1 | Contributor | +10% (has valid circle) |
| 2 | Ambassador | +25% (5+ referrals) |
| 3 | NodeOperator | +50% (runs node software) |
#### MinerStatus
| Value | Name |
|-------|------|
| 0 | Active |
| 1 | Suspended |
| 2 | Banned |
#### MiningSessionStatus
| Value | Name |
|-------|------|
| 0 | Active |
| 1 | Completed |
| 2 | Claimed |
| 3 | Expired |
#### CircleStatus
| Value | Name |
|-------|------|
| 0 | Incomplete (< 3 members) |
| 1 | Active (3-5 members, valid for bonus) |
| 2 | Disbanded |
### Domain Exceptions
- `MiningDomainException` -- base exception
- `MinerNotFoundException` -- miner not found by ID or UserId
- `CircleDomainException` -- circle operation failures
- `ReferralDomainException` -- referral operation failures
---
## 6. Database Schema
**Database**: `mining_service` (PostgreSQL)
**Migration**: `20260117103924_InitialCreate`
### Table: `Miners`
| Column | Type | Constraints |
|--------|------|-------------|
| `Id` | uuid | PK |
| `UserId` | uuid | NOT NULL, UNIQUE (IX_Miners_UserId) |
| `Role` | varchar(20) | NOT NULL, string conversion of MinerRole enum |
| `TotalMinedPoints` | numeric(18,4) | NOT NULL |
| `CurrentRate_BaseRate` | numeric(18,4) | NOT NULL (owned type) |
| `CurrentRate_RoleBonus` | numeric(5,4) | NOT NULL (owned type) |
| `CurrentRate_CircleBonus` | numeric(5,4) | NOT NULL (owned type) |
| `CurrentRate_ReferralBonus` | numeric(5,4) | NOT NULL (owned type) |
| `CurrentRate_StreakBonus` | numeric(5,4) | NOT NULL (owned type) |
| `ActiveSession_SessionId` | uuid | NULLABLE (owned type) |
| `ActiveSession_StartTime` | timestamp with time zone | NULLABLE |
| `ActiveSession_EndTime` | timestamp with time zone | NULLABLE |
| `ActiveSession_HourlyRate` | numeric(18,4) | NULLABLE |
| `ActiveSession_Status` | varchar(20) | NULLABLE |
| `ActiveSession_AccumulatedPoints` | numeric(18,4) | NULLABLE |
| `Streak_CurrentStreak` | integer | NOT NULL (owned type) |
| `Streak_LongestStreak` | integer | NOT NULL |
| `Streak_LastMiningDate` | timestamp with time zone | NOT NULL |
| `Streak_FreezeTokens` | integer | NOT NULL |
| `Streak_IsGracePeriod` | boolean | NOT NULL |
| `ReferralCode` | varchar(10) | NOT NULL, UNIQUE (IX_Miners_ReferralCode) |
| `ReferredById` | uuid | NULLABLE |
| `CircleId` | uuid | NULLABLE |
| `Status` | varchar(20) | NOT NULL |
| `CreatedAt` | timestamp with time zone | NOT NULL |
| `UpdatedAt` | timestamp with time zone | NOT NULL |
| `RowVersion` | bytea | NOT NULL, row version (concurrency token) |
**Indexes**: `IX_Miners_UserId` (unique), `IX_Miners_ReferralCode` (unique)
### Table: `MiningHistories`
| Column | Type | Constraints |
|--------|------|-------------|
| `Id` | uuid | PK |
| `MinerId` | uuid | NOT NULL, FK -> Miners (CASCADE) |
| `PointsEarned` | numeric(18,4) | NOT NULL |
| `Source` | varchar(50) | NOT NULL |
| `SessionId` | uuid | NULLABLE |
| `EarnedAt` | timestamp with time zone | NOT NULL |
| `HourlyRateSnapshot` | numeric(18,4) | NOT NULL |
| `StreakDaySnapshot` | integer | NOT NULL |
**Indexes**: `IX_MiningHistories_MinerId_EarnedAt` (composite)
### Table: `Circles`
| Column | Type | Constraints |
|--------|------|-------------|
| `Id` | uuid | PK |
| `OwnerId` | uuid | NOT NULL |
| `Name` | varchar(100) | NOT NULL |
| `TrustScore` | numeric(5,2) | NOT NULL |
| `BonusMultiplier` | numeric(5,4) | NOT NULL |
| `Status` | varchar(20) | NOT NULL |
| `CreatedAt` | timestamp with time zone | NOT NULL |
| `UpdatedAt` | timestamp with time zone | NOT NULL |
**Indexes**: `IX_Circles_OwnerId`
### Table: `CircleMembers`
| Column | Type | Constraints |
|--------|------|-------------|
| `Id` | uuid | PK |
| `CircleId` | uuid | NOT NULL, FK -> Circles (CASCADE) |
| `MinerId` | uuid | NOT NULL |
| `JoinedAt` | timestamp with time zone | NOT NULL |
| `IsActive` | boolean | NOT NULL |
**Indexes**: `IX_CircleMembers_CircleId_MinerId` (unique composite), `IX_CircleMembers_MinerId`
### Table: `Referrals`
| Column | Type | Constraints |
|--------|------|-------------|
| `Id` | uuid | PK |
| `ReferrerId` | uuid | NOT NULL |
| `ReferredId` | uuid | NOT NULL, UNIQUE (IX_Referrals_ReferredId) |
| `ReferralCode` | varchar(10) | NOT NULL |
| `BonusRate` | numeric(5,4) | NOT NULL |
| `IsActive` | boolean | NOT NULL |
| `Level` | integer | NOT NULL |
| `CreatedAt` | timestamp with time zone | NOT NULL |
| `ActivatedAt` | timestamp with time zone | NULLABLE |
**Indexes**: `IX_Referrals_ReferrerId`, `IX_Referrals_ReferredId` (unique)
**Note**: Table names use PascalCase (e.g., `Miners`, `MiningHistories`), not the platform-standard snake_case. Value objects (MiningRate, MiningStreak, MiningSession) are stored as owned types with `{OwnerProperty}_{ValueProperty}` column naming.
---
## 7. Integration Events
### Published Events (outbound)
| Event | Fields | Description |
|-------|--------|-------------|
| `PointsMinedIntegrationEvent` | `EventId`, `OccurredOn`, `UserId`, `MinerId`, `Points`, `Source`, `StreakDays` | Points mined, should be credited to wallet |
| `ReferralActivatedIntegrationEvent` | `EventId`, `OccurredOn`, `ReferrerId`, `ReferredUserId`, `BonusRate` | Referral activated after KYC |
| `CircleCompletedIntegrationEvent` | `EventId`, `OccurredOn`, `CircleId`, `OwnerId`, `MemberCount`, `BonusMultiplier` | Security circle completed (3+ members) |
### Consumed Events (inbound)
| Event | Handler | Description |
|-------|---------|-------------|
| `UserRegisteredIntegrationEvent` | `UserRegisteredIntegrationEventHandler` | Creates new Miner profile when user registers in IAM. If referral code provided, creates Referral relationship. |
| `UserKycCompletedIntegrationEvent` | `UserKycCompletedIntegrationEventHandler` | Activates pending referral for user who completed KYC verification. |
**Note**: Integration events are defined as records implementing `IIntegrationEvent`. Handlers use MediatR notification wrappers (`UserRegisteredNotification`, `UserKycCompletedNotification`). The actual message broker (RabbitMQ) integration for publishing/consuming is not yet wired up -- the event definitions and handlers exist but the transport layer is missing.
### Domain Events
| Event | Fields | Raised By |
|-------|--------|-----------|
| `MinerCreatedDomainEvent` | `MinerId`, `UserId` | `Miner.Create()` |
| `MiningSessionStartedDomainEvent` | `MinerId`, `SessionId`, `HourlyRate` | `Miner.StartMiningSession()` |
| `PointsMinedDomainEvent` | `MinerId`, `PointsEarned`, `TotalPoints`, `StreakDays` | `Miner.ClaimMiningReward()` |
| `StreakUpdatedDomainEvent` | `MinerId`, `PreviousStreak`, `NewStreak`, `NewBonusMultiplier` | Defined but not raised in current code |
| `CircleCompletedDomainEvent` | `CircleId`, `OwnerId`, `MemberCount` | `Circle.RecalculateStatus()` (when reaching 3 members) |
| `ReferralActivatedDomainEvent` | `ReferralId`, `ReferrerId`, `ReferredId` | `Referral.Activate()` |
| `ConfigurationUpdatedDomainEvent` | `ConfigType`, `UpdatedBy`, `UpdatedAt` | Defined but not raised in current code |
---
## 8. Dependencies
### NuGet Packages
**API Layer** (`MiningService.API.csproj`):
| Package | Version |
|---------|---------|
| MediatR | 12.4.1 |
| FluentValidation | 11.11.0 |
| FluentValidation.DependencyInjectionExtensions | 11.11.0 |
| Microsoft.AspNetCore.Authentication.JwtBearer | 10.0.2 |
| Microsoft.EntityFrameworkCore.Design | 10.0.2 |
| Microsoft.Extensions.Http.Polly | 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 |
**Infrastructure Layer** (`MiningService.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 |
**Domain Layer** (`MiningService.Domain.csproj`):
| Package | Version |
|---------|---------|
| MediatR.Contracts | 2.0.1 |
### External Service Dependencies
| Service | Default URL | Client Interface | Purpose |
|---------|-------------|-----------------|---------|
| IAM Service | `http://iam-service-net:8080` | `IIamServiceClient` | Get user info, validate user, KYC status |
| Wallet Service | `http://wallet-service-net:8080` | `IWalletServiceClient` | Transfer mined points to wallet, check balance |
| Social Service | `http://social-service-net:8080` | `ISocialServiceClient` | Friend suggestions for circles, friendship checks |
All external clients use Polly resilience policies:
- **Retry**: 3 retries with exponential backoff (2s, 4s, 8s)
- **Circuit Breaker**: Opens after 5 failures, stays open for 30s
---
## 9. Configuration
### Environment Variables / appsettings.json
| Key | Default | Description |
|-----|---------|-------------|
| `ConnectionStrings:DefaultConnection` | Neon PostgreSQL connection string | Primary database connection |
| `DATABASE_URL` | -- | Fallback database connection string |
| `Redis:ConnectionString` | `localhost:6379` | Redis connection (declared but not actively used in repositories) |
| `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 | Token expiry (declared, not used internally) |
| `Jwt:RefreshTokenExpiryDays` | 7 | Refresh token expiry (declared, not used internally) |
| `ExternalServices:IamService:BaseUrl` | `http://iam-service-net:8080` | IAM service URL |
| `ExternalServices:WalletService:BaseUrl` | `http://wallet-service-net:8080` | Wallet service URL |
| `ExternalServices:SocialService:BaseUrl` | `http://social-service-net:8080` | Social service URL |
| `AllowedOrigins` | `["http://localhost:3000", "http://localhost:5173"]` | CORS allowed origins |
| `ASPNETCORE_ENVIRONMENT` | `Development` | Environment name |
| `ASPNETCORE_URLS` | `http://+:8080` (Docker) | Listen URLs |
### MediatR Pipeline Order
1. `LoggingBehavior` -- logs request name, elapsed time
2. `ValidatorBehavior` -- runs FluentValidation (no validators currently defined)
3. `TransactionBehavior` -- wraps Commands in DB transaction (skips Queries by name suffix)
### Docker
- Multi-stage build: `sdk:10.0` (build) -> `aspnet:10.0` (runtime)
- Non-root user: `dotnetuser` (UID/GID 1001)
- Port: 8080
- Health check: `curl -f http://localhost:8080/health/live` (30s interval, 3 retries)
### Database Startup
- Auto-applies EF Core migrations on startup (`dbContext.Database.MigrateAsync()`)
- Npgsql retry on failure: 5 retries, 30s max delay
### Idempotency
- `ClientRequest` entity and `RequestManager` are defined but not wired into the command pipeline (no commands use idempotency checks).
### Known Gaps / Stubs
- **No FluentValidation validators** are defined for any command
- **ConfigurationRepository** is purely in-memory (static fields with lock); database persistence for configs is not implemented
- **Analytics queries** for Points, Streaks, and AuditLogs have no handlers
- **BanMinerCommand** handler calls `Suspend()` instead of `Ban()`
- **ResetMinerStreakCommand** handler does not actually reset the streak
- **UpdateSystemConfigCommand** and **UpdateMiningConfigCommand** handlers do not persist changes
- **RabbitMQ integration** for publishing/consuming integration events is not connected
- **Redis** is declared as a dependency but not used in any repository or service
- **Dapper** is referenced but no raw SQL queries exist
- **Table names** use PascalCase instead of the platform-standard snake_case