# 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