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>
560 lines
30 KiB
Markdown
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
|