# SocialService - Service Documentation

## 1. Overview

The Social Service manages user-to-user social relationships within the GoodGo platform. It handles friendships (bidirectional, requires acceptance), following (unidirectional, auto-accepted), user blocking, and graph-based queries such as mutual friends and friend suggestions.

- **Service Name**: social-service-net
- **Port**: 5009 (Development, HTTP)
- **Docker Port**: 8080
- **Database**: PostgreSQL - `social_service` (Neon cloud)
- **Database (local)**: configurable via `DATABASE_URL` env var
- **Framework**: .NET 10.0, C# 14
- **Architecture**: Clean Architecture + CQRS (MediatR)
- **Solution File**: `SocialService.slnx`
- **Migration**: `20260112164356_InitialCreate` (auto-applied on startup)

---

## 2. API Endpoints

### 2.1 RelationshipsController

**Base Route**: `api/v1/relationships`

| Method | Route | Description | Request Body | Response |
|--------|-------|-------------|-------------|----------|
| GET | `/users/{userId}/friends?skip=0&take=20` | Get friends of a user (paginated) | - | `GetFriendsResult` |
| POST | `/friend-requests` | Send a friend request | `SendFriendRequestRequest` | `SendFriendRequestResult` (201) |
| PUT | `/friend-requests/{relationshipId}` | Accept or reject a friend request | `RespondToFriendRequestRequest` | `{ success: bool }` |
| GET | `/users/{userId1}/mutual-friends/{userId2}` | Get mutual friends between two users | - | `GetMutualFriendsResult` |
| GET | `/users/{userId}/suggestions?limit=10` | Get friend suggestions for a user | - | `GetFriendSuggestionsResult` |
| POST | `/follow` | Follow a user | `FollowUserRequest` | `FollowUserResult` (201) |
| DELETE | `/follow` | Unfollow a user | `UnfollowUserRequest` | `{ success: bool }` |

**Request DTOs** (defined in RelationshipsController.cs):
- `SendFriendRequestRequest(Guid RequesterId, Guid AddresseeId)`
- `RespondToFriendRequestRequest(Guid UserId, bool Accept)`
- `FollowUserRequest(Guid FollowerId, Guid FolloweeId)`
- `UnfollowUserRequest(Guid FollowerId, Guid FolloweeId)`

### 2.2 BlocksController

**Base Route**: `api/v1/blocks`

| Method | Route | Description | Request Body | Response |
|--------|-------|-------------|-------------|----------|
| POST | `/` | Block a user | `BlockUserRequest` | `BlockUserResult` (201) |
| DELETE | `/` | Unblock a user | `UnblockUserRequest` | `{ success: bool }` |
| GET | `/users/{userId}?skip=0&take=20` | Get list of blocked users (paginated) | - | `GetBlockedUsersResult` |

**Request DTOs** (defined in BlocksController.cs):
- `BlockUserRequest(Guid BlockerId, Guid BlockedId, string? Reason = null)`
- `UnblockUserRequest(Guid BlockerId, Guid BlockedId)`

### 2.3 AdminController

**Base Route**: `api/v1/admin/social`

| Method | Route | Description | Query Params | Response |
|--------|-------|-------------|-------------|----------|
| GET | `/relationships?skip=0&take=20&status=&type=` | Get all relationships (filtered, paginated) | `skip`, `take`, `status` (pending/accepted/rejected/cancelled), `type` (friendship/following) | `GetAllRelationshipsResult` |
| GET | `/relationships/{id:guid}` | Get relationship by ID | - | `RelationshipDetailDto` or 404 |
| DELETE | `/relationships/{id:guid}` | Admin delete a relationship | - | `{ success, message }` or 404 |
| GET | `/blocks?skip=0&take=20` | Get all blocks (paginated) | `skip`, `take` | `GetAllBlocksResult` |
| DELETE | `/blocks/{id:guid}` | Admin delete a block | - | `{ success, message }` or 404 |
| GET | `/statistics` | Get social statistics overview | - | `SocialStatisticsDto` |

### 2.4 Health Check Endpoints

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

---

## 3. Commands

All commands are MediatR `IRequest<T>` records. The MediatR pipeline wraps commands in transactions automatically (via `TransactionBehavior`, which skips requests ending in "Query").

### 3.1 SendFriendRequestCommand
- **File**: `Application/Commands/SendFriendRequestCommand.cs`
- **Parameters**: `Guid RequesterId`, `Guid AddresseeId`
- **Returns**: `SendFriendRequestResult(Guid RelationshipId, string Status)`
- **Behavior**:
  - Checks for blocks between users (throws if blocked)
  - Checks for existing friendship (throws if already friends or pending)
  - If reverse pending request exists, auto-accepts it (mutual request)
  - Otherwise creates new Relationship with `Pending` status
- **Dependencies**: `IRelationshipRepository`, `IUserBlockRepository`

### 3.2 RespondToFriendRequestCommand
- **File**: `Application/Commands/RespondToFriendRequestCommand.cs`
- **Parameters**: `Guid RelationshipId`, `Guid UserId`, `bool Accept`
- **Returns**: `bool`
- **Behavior**:
  - Verifies relationship exists (throws if not found)
  - Verifies user is the addressee (throws if not)
  - Calls `Accept()` or `Reject()` on the Relationship entity
- **Dependencies**: `IRelationshipRepository`

### 3.3 FollowUserCommand
- **File**: `Application/Commands/FollowUserCommand.cs`
- **Parameters**: `Guid FollowerId`, `Guid FolloweeId`
- **Returns**: `FollowUserResult(Guid RelationshipId, bool Success)`
- **Behavior**:
  - Prevents self-follow
  - Checks for blocks between users
  - Checks for existing follow (throws if already following)
  - Creates Relationship with `Following` type, calls `Accept()` (auto-accepted)
- **Dependencies**: `IRelationshipRepository`, `IUserBlockRepository`

### 3.4 UnfollowUserCommand
- **File**: `Application/Commands/UnfollowUserCommand.cs`
- **Parameters**: `Guid FollowerId`, `Guid FolloweeId`
- **Returns**: `bool`
- **Behavior**:
  - Finds existing Following relationship (throws if not following)
  - Calls `Remove()` on the Relationship entity (sets status to Cancelled)
- **Dependencies**: `IRelationshipRepository`

### 3.5 BlockUserCommand
- **File**: `Application/Commands/BlockUserCommand.cs`
- **Parameters**: `Guid BlockerId`, `Guid BlockedId`, `string? Reason = null`
- **Returns**: `BlockUserResult(Guid BlockId, bool Success)`
- **Behavior**:
  - Prevents self-block
  - Checks for existing block (throws if already blocked)
  - Creates UserBlock entity
  - Removes all existing relationships between the two users (friendships and followings in both directions) by calling `Remove()` on each
- **Dependencies**: `IUserBlockRepository`, `IRelationshipRepository`

### 3.6 UnblockUserCommand
- **File**: `Application/Commands/UnblockUserCommand.cs`
- **Parameters**: `Guid BlockerId`, `Guid BlockedId`
- **Returns**: `bool`
- **Behavior**:
  - Finds existing block (throws if not blocked)
  - Removes the UserBlock entity
- **Dependencies**: `IUserBlockRepository`

### 3.7 AdminDeleteRelationshipCommand
- **File**: `Application/Commands/AdminDeleteRelationshipCommand.cs`
- **Parameters**: `Guid RelationshipId`
- **Returns**: `bool`
- **Behavior**: Hard-deletes a relationship from the database. Returns false if not found.
- **Dependencies**: `IRelationshipRepository`

### 3.8 AdminDeleteBlockCommand
- **File**: `Application/Commands/AdminDeleteBlockCommand.cs`
- **Parameters**: `Guid BlockId`
- **Returns**: `bool`
- **Behavior**: Hard-deletes a block from the database. Returns false if not found.
- **Dependencies**: `IUserBlockRepository`

---

## 4. Queries

### 4.1 GetFriendsQuery
- **File**: `Application/Queries/GetFriendsQuery.cs`
- **Parameters**: `Guid UserId`, `int Skip = 0`, `int Take = 20`
- **Returns**: `GetFriendsResult(IEnumerable<FriendDto> Friends, int TotalCount)`
- **FriendDto**: `(Guid UserId, string DisplayName, string? AvatarUrl, DateTime FriendsSince)`
- **Behavior**: Gets accepted friendships where user is requester or addressee, enriches with UserProfile data.

### 4.2 GetMutualFriendsQuery
- **File**: `Application/Queries/GetMutualFriendsQuery.cs`
- **Parameters**: `Guid UserId1`, `Guid UserId2`
- **Returns**: `GetMutualFriendsResult(IEnumerable<FriendDto> MutualFriends, int Count)`
- **Behavior**: Uses `IGraphQueryService` with raw SQL (CTE) to find users who are friends with both users. Enriches with UserProfile data.

### 4.3 GetFriendSuggestionsQuery
- **File**: `Application/Queries/GetFriendSuggestionsQuery.cs`
- **Parameters**: `Guid UserId`, `int Limit = 10`
- **Returns**: `GetFriendSuggestionsResult(IEnumerable<FriendSuggestionDto> Suggestions)`
- **FriendSuggestionDto**: `(Guid UserId, string DisplayName, string? AvatarUrl, int MutualFriendsCount)`
- **Behavior**: Uses `IGraphQueryService` with raw SQL (friends-of-friends CTE). Excludes already-friends and blocked users. Orders by mutual friend count descending.

### 4.4 GetBlockedUsersQuery
- **File**: Defined in `Controllers/BlocksController.cs`
- **Parameters**: `Guid UserId`, `int Skip = 0`, `int Take = 20`
- **Returns**: `GetBlockedUsersResult(IEnumerable<BlockedUserDto> BlockedUsers, int TotalCount)`
- **BlockedUserDto**: `(Guid UserId, string DisplayName, string? AvatarUrl, DateTime BlockedAt, string? Reason)`
- **Behavior**: Gets blocks by blocker user ID, enriches with UserProfile data.

### 4.5 GetAllRelationshipsQuery (Admin)
- **File**: `Application/Queries/GetAllRelationshipsQuery.cs`
- **Parameters**: `int Skip`, `int Take`, `string? StatusFilter`, `string? TypeFilter`
- **Returns**: `GetAllRelationshipsResult(IEnumerable<RelationshipAdminDto> Relationships, int TotalCount)`
- **RelationshipAdminDto**: `(Guid Id, Guid RequesterId, Guid AddresseeId, string Type, string Status, DateTime CreatedAt, DateTime? UpdatedAt)`

### 4.6 GetRelationshipByIdQuery (Admin)
- **File**: `Application/Queries/GetRelationshipByIdQuery.cs`
- **Parameters**: `Guid Id`
- **Returns**: `RelationshipDetailDto?` (nullable)
- **RelationshipDetailDto**: `(Guid Id, Guid RequesterId, Guid AddresseeId, string Type, string Status, DateTime CreatedAt, DateTime? UpdatedAt)`

### 4.7 GetAllBlocksQuery (Admin)
- **File**: `Application/Queries/GetAllBlocksQuery.cs`
- **Parameters**: `int Skip`, `int Take`
- **Returns**: `GetAllBlocksResult(IEnumerable<BlockAdminDto> Blocks, int TotalCount)`
- **BlockAdminDto**: `(Guid Id, Guid BlockerId, Guid BlockedId, string? Reason, DateTime CreatedAt)`

### 4.8 GetSocialStatisticsQuery (Admin)
- **File**: `Application/Queries/GetSocialStatisticsQuery.cs`
- **Parameters**: none
- **Returns**: `SocialStatisticsDto(int TotalFriendships, int TotalFollowings, int TotalBlocks, int PendingFriendRequests, int AcceptedFriendships, int TotalRelationships)`

---

## 5. Domain Model

### 5.1 Aggregates

#### Relationship (Aggregate Root)
- **File**: `Domain/AggregatesModel/RelationshipAggregate/Relationship.cs`
- **Extends**: `Entity`, implements `IAggregateRoot`
- **Private Fields**: `_requesterId`, `_addresseeId`, `_type`, `_status`, `_createdAt`, `_updatedAt`
- **Public Properties**: `RequesterId`, `AddresseeId`, `Type`, `TypeId`, `Status`, `StatusId`, `CreatedAt`, `UpdatedAt`
- **Constructor**: `Relationship(Guid requesterId, Guid addresseeId, RelationshipType type)`
  - Validates: no empty GUIDs, no self-relationship
  - Following type: auto-sets status to `Accepted`, raises `UserFollowedDomainEvent`
  - Friendship type: sets status to `Pending`, raises `FriendRequestSentDomainEvent`
- **Behavior Methods**:
  - `Accept()` - Only for pending friendships. Sets status to Accepted. Raises `FriendshipCreatedDomainEvent` and `RelationshipStatusChangedDomainEvent`.
  - `Reject()` - Only for pending friendships. Sets status to Rejected. Raises `RelationshipStatusChangedDomainEvent`.
  - `Cancel()` - Only for pending requests. Sets status to Cancelled. Raises `RelationshipStatusChangedDomainEvent`.
  - `Remove()` - Only for accepted relationships. Sets status to Cancelled. Raises `RelationshipRemovedDomainEvent` and `RelationshipStatusChangedDomainEvent`.

#### UserBlock (Aggregate Root)
- **File**: `Domain/AggregatesModel/UserBlockAggregate/UserBlock.cs`
- **Extends**: `Entity`, implements `IAggregateRoot`
- **Private Fields**: `_blockerId`, `_blockedId`, `_reason`, `_createdAt`
- **Public Properties**: `BlockerId`, `BlockedId`, `Reason`, `CreatedAt`
- **Constructor**: `UserBlock(Guid blockerId, Guid blockedId, string? reason = null)`
  - Validates: no empty GUIDs, no self-block
  - Raises `UserBlockedDomainEvent`
- **No behavior methods** (immutable once created; removal is done via repository `Remove()`)

#### UserProfile (Aggregate Root)
- **File**: `Domain/AggregatesModel/UserProfileAggregate/UserProfile.cs`
- **Extends**: `Entity`, implements `IAggregateRoot`
- **Private Fields**: `_userId`, `_displayName`, `_avatarUrl`, `_bio`, `_lastSyncedAt`, `_createdAt`
- **Public Properties**: `UserId`, `DisplayName`, `AvatarUrl`, `Bio`, `LastSyncedAt`, `CreatedAt`
- **Constructor**: `UserProfile(Guid userId, string displayName, string? avatarUrl = null, string? bio = null)`
- **Behavior Methods**:
  - `UpdateFromEvent(string displayName, string? avatarUrl, string? bio)` - Updates profile data from IAM integration event

### 5.2 Enumerations (Type-Safe Enum Pattern)

#### RelationshipType
- **File**: `Domain/AggregatesModel/RelationshipAggregate/RelationshipType.cs`
- **Values**:
  - `Friendship` (Id: 1) - Bidirectional, requires acceptance
  - `Following` (Id: 2) - Unidirectional, auto-accepted

#### RelationshipStatus
- **File**: `Domain/AggregatesModel/RelationshipAggregate/RelationshipStatus.cs`
- **Values**:
  - `Pending` (Id: 1) - Waiting for acceptance
  - `Accepted` (Id: 2) - Active relationship
  - `Rejected` (Id: 3) - Request was rejected
  - `Cancelled` (Id: 4) - Cancelled by requester or removed

### 5.3 Domain Services

#### IGraphQueryService
- **File**: `Domain/Services/IGraphQueryService.cs`
- **Implementation**: `Infrastructure/Services/GraphQueryService.cs` (PostgreSQL raw SQL with CTEs)
- **Methods**:
  - `GetMutualFriendsAsync(Guid userId1, Guid userId2)` - Returns mutual friend IDs using SQL CTE joins
  - `GetFriendSuggestionsAsync(Guid userId, int limit)` - Friends-of-friends who aren't already friends, excludes blocked users, ordered by mutual count
  - `GetConnectionDegreeAsync(Guid userId1, Guid userId2)` - Recursive CTE BFS, max depth 6, returns degrees of separation (-1 if not connected)
  - `GetMutualFriendsCountAsync(Guid userId1, Guid userId2)` - Count of mutual friends
- **Helper DTO**: `FriendSuggestion(Guid UserId, int MutualFriendsCount)`

### 5.4 Domain Events

**Relationship Events** (`Domain/Events/RelationshipDomainEvents.cs`):
- `FriendRequestSentDomainEvent` - Raised when a friendship request is created
- `FriendshipCreatedDomainEvent` - Raised when a friendship request is accepted
- `UserFollowedDomainEvent` - Raised when a follow relationship is created
- `RelationshipStatusChangedDomainEvent` - Raised on any status transition (includes PreviousStatus and NewStatus)
- `RelationshipRemovedDomainEvent` - Raised when an accepted relationship is removed

**Block Events** (`Domain/Events/UserBlockDomainEvents.cs`):
- `UserBlockedDomainEvent` - Raised when a user block is created
- `UserUnblockedDomainEvent` - Defined but not currently raised in code (the UnblockUserCommandHandler does not add this event)

### 5.5 Exceptions

- `DomainException` - Base exception class
- `SocialDomainException` - Extends DomainException, used for all social domain rule violations
- `SampleDomainException` - Leftover from template, extends DomainException (unused)

### 5.6 SeedWork

Base classes shared across the domain:
- `Entity` - Base entity with `Id` (Guid), `DomainEvents` collection, equality by ID
- `IAggregateRoot` - Marker interface
- `IRepository<T>` - Generic repository with `IUnitOfWork UnitOfWork`
- `IUnitOfWork` - `SaveChangesAsync()`, `SaveEntitiesAsync()` (dispatches domain events)
- `Enumeration` - Type-safe enum base class with `Id`, `Name`, `GetAll<T>()`, `FromValue<T>()`, `FromDisplayName<T>()`
- `ValueObject` - Immutable value comparison base class (not currently used by any entity)

---

## 6. Database Schema

**Database**: `social_service` (PostgreSQL)
**Migration**: `20260112164356_InitialCreate`

### 6.1 Table: `relationships`

| Column | Type | Nullable | Constraints |
|--------|------|----------|-------------|
| `id` | uuid | NO | PK, ValueGeneratedNever |
| `requester_id` | uuid | NO | |
| `addressee_id` | uuid | NO | |
| `type_id` | integer | NO | FK -> relationship_types.id (RESTRICT) |
| `status_id` | integer | NO | FK -> relationship_statuses.id (RESTRICT) |
| `created_at` | timestamp with time zone | NO | |
| `updated_at` | timestamp with time zone | YES | |

**Indexes**:
- `ix_relationships_requester_addressee_type` - UNIQUE on (requester_id, addressee_id, type_id)
- `ix_relationships_requester` - on (requester_id)
- `ix_relationships_addressee` - on (addressee_id)
- `ix_relationships_type_status` - on (type_id, status_id)
- `IX_relationships_status_id` - on (status_id) (auto-generated by EF for FK)

### 6.2 Table: `relationship_types` (Enumeration)

| Column | Type | Nullable | Constraints |
|--------|------|----------|-------------|
| `id` | integer | NO | PK, ValueGeneratedNever |
| `name` | varchar(50) | NO | |

**Seed Data**: `(1, "Friendship")`, `(2, "Following")`

### 6.3 Table: `relationship_statuses` (Enumeration)

| Column | Type | Nullable | Constraints |
|--------|------|----------|-------------|
| `id` | integer | NO | PK, ValueGeneratedNever |
| `name` | varchar(50) | NO | |

**Seed Data**: `(1, "Pending")`, `(2, "Accepted")`, `(3, "Rejected")`, `(4, "Cancelled")`

### 6.4 Table: `user_blocks`

| Column | Type | Nullable | Constraints |
|--------|------|----------|-------------|
| `id` | uuid | NO | PK, ValueGeneratedNever |
| `blocker_id` | uuid | NO | |
| `blocked_id` | uuid | NO | |
| `reason` | varchar(500) | YES | |
| `created_at` | timestamp with time zone | NO | |

**Indexes**:
- `ix_user_blocks_blocker_blocked` - UNIQUE on (blocker_id, blocked_id)
- `ix_user_blocks_blocker` - on (blocker_id)
- `ix_user_blocks_blocked` - on (blocked_id)

### 6.5 Table: `user_profiles`

| Column | Type | Nullable | Constraints |
|--------|------|----------|-------------|
| `id` | uuid | NO | PK, ValueGeneratedNever |
| `user_id` | uuid | NO | |
| `display_name` | varchar(255) | NO | |
| `avatar_url` | varchar(500) | YES | |
| `bio` | varchar(500) | YES | |
| `last_synced_at` | timestamp with time zone | NO | |
| `created_at` | timestamp with time zone | NO | |

**Indexes**:
- `ix_user_profiles_user_id` - UNIQUE on (user_id)
- `ix_user_profiles_display_name` - on (display_name)

---

## 7. Integration Events

### 7.1 Domain Events (Internal)

The following domain events are raised within the service and dispatched via MediatR `INotification` through the `SocialServiceContext.DispatchDomainEventsAsync()` method before `SaveChanges`. Currently, there are **no domain event handlers** registered in the service -- the events are raised but not consumed.

- `FriendRequestSentDomainEvent`
- `FriendshipCreatedDomainEvent`
- `UserFollowedDomainEvent`
- `RelationshipStatusChangedDomainEvent`
- `RelationshipRemovedDomainEvent`
- `UserBlockedDomainEvent`

### 7.2 Cross-Service Integration

- **Inbound (Expected)**: `UserProfile.UpdateFromEvent()` method exists for receiving IAM profile update events. However, there is **no integration event handler or RabbitMQ consumer implemented** -- the UserProfile entity and repository exist but have no controllers or commands for creating/updating profiles. Profiles must currently be populated directly in the database.
- **Outbound**: No integration events are published to RabbitMQ or any message broker. Domain events remain internal only.

---

## 8. Dependencies

### 8.1 NuGet Packages

**SocialService.API**:
- MediatR 12.4.1
- FluentValidation 11.11.0
- FluentValidation.DependencyInjectionExtensions 11.11.0
- Microsoft.EntityFrameworkCore.Design 10.0.1
- 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

**SocialService.Domain**:
- MediatR.Contracts 2.0.1

**SocialService.Infrastructure**:
- Microsoft.EntityFrameworkCore 10.0.0
- Npgsql.EntityFrameworkCore.PostgreSQL 10.0.0
- Microsoft.EntityFrameworkCore.Tools 10.0.0
- MediatR 12.4.1
- Dapper 2.1.35
- Microsoft.Extensions.Http.Polly 9.0.0
- Polly 8.5.0
- StackExchange.Redis 2.8.16

**Shared (Directory.Build.props)**:
- Microsoft.SourceLink.GitHub 8.0.0
- Target: net10.0, LangVersion 14.0, Nullable enabled, TreatWarningsAsErrors

**Unit Tests**:
- Microsoft.NET.Test.Sdk 17.12.0
- xunit 2.9.2
- FluentAssertions 6.12.2
- NSubstitute 5.3.0
- coverlet.collector 6.0.2

**Functional Tests**:
- Microsoft.AspNetCore.Mvc.Testing 10.0.0
- Microsoft.EntityFrameworkCore.InMemory 10.0.0
- FluentAssertions 6.12.2
- Testcontainers.PostgreSql 4.1.0

### 8.2 External Service Dependencies

- **PostgreSQL** (Neon cloud or local) - primary data store
- **Redis** (referenced in health checks and .env.example, but **not actively used** in code -- no Redis caching is implemented)
- **IAM Service** - UserProfile entity is designed to sync from IAM, but no integration is implemented

---

## 9. Configuration

### 9.1 appsettings.json

```json
{
    "ConnectionStrings": {
        "DefaultConnection": "Host=...neon.tech;Database=social_service;..."
    },
    "Redis": {
        "ConnectionString": "localhost:6379"
    },
    "Jwt": {
        "Secret": "your-super-secret-key-min-32-characters",
        "Issuer": "goodgo-platform",
        "Audience": "goodgo-services",
        "AccessTokenExpiryMinutes": 15,
        "RefreshTokenExpiryDays": 7
    }
}
```

**Note**: JWT configuration is present in settings but **JWT authentication middleware is NOT configured** in `Program.cs`. There is no `AddAuthentication()` or `UseAuthentication()` call. All endpoints are currently unauthenticated.

### 9.2 Environment Variables (.env.example)

| Variable | Description | Default |
|----------|-------------|---------|
| `ASPNETCORE_ENVIRONMENT` | Environment name | Development |
| `DATABASE_URL` | PostgreSQL connection string | localhost:5432 |
| `REDIS_URL` | Redis connection string | localhost:6379 |
| `REDIS_PASSWORD` | Redis password | (empty) |
| `JWT_SECRET` | JWT signing key | (placeholder) |
| `JWT_ISSUER` | JWT issuer | goodgo-platform |
| `JWT_AUDIENCE` | JWT audience | goodgo-services |
| `JWT_ACCESS_TOKEN_EXPIRY_MINUTES` | Access token TTL | 15 |
| `JWT_REFRESH_TOKEN_EXPIRY_DAYS` | Refresh token TTL | 7 |
| `API_PORT` | API port | 5000 |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | OpenTelemetry endpoint | localhost:4317 |
| `OTEL_SERVICE_NAME` | OpenTelemetry service name | socialservice |
| `SEQ_URL` | Seq logging endpoint | localhost:5341 |
| `RATE_LIMIT_PERMITS_PER_MINUTE` | Rate limit | 100 |
| `RATE_LIMIT_QUEUE_LIMIT` | Rate limit queue | 10 |
| `HEALTHCHECK_TIMEOUT_SECONDS` | Health check timeout | 5 |

### 9.3 MediatR Pipeline (Behavior Order)

1. `LoggingBehavior` - Logs request name, measures execution time with Stopwatch
2. `ValidatorBehavior` - Runs FluentValidation validators (none currently registered)
3. `TransactionBehavior` - Wraps commands in a database transaction (skips requests ending in "Query"), uses `CreateExecutionStrategy()` for retry support

### 9.4 DI Registration (Infrastructure)

Registered in `DependencyInjection.AddInfrastructure()`:
- `SocialServiceContext` (DbContext with Npgsql, retry on failure: 5 retries, 30s delay)
- `IRelationshipRepository` -> `RelationshipRepository` (Scoped)
- `IUserProfileRepository` -> `UserProfileRepository` (Scoped)
- `IUserBlockRepository` -> `UserBlockRepository` (Scoped)
- `IGraphQueryService` -> `GraphQueryService` (Scoped)
- `IRequestManager` -> `RequestManager` (Scoped)

### 9.5 Docker Configuration

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

### 9.6 SDK Version

- .NET SDK: 10.0.101 (rollForward: latestMinor)

---

## 10. Test Coverage

### 10.1 Unit Tests (`SocialService.UnitTests`)

**Handler Tests**:
- `SendFriendRequestCommandHandlerTests` (4 tests):
  - Valid request creates new relationship
  - Block exists throws exception
  - Already friends throws exception
  - Request already pending throws exception
  - Mutual request auto-accepts

- `BlockUserCommandHandlerTests` (5 tests):
  - Valid request creates block
  - Self-block throws exception
  - Already blocked throws exception
  - With existing friendship removes friendship
  - No reason creates block without reason

**Domain Tests**:
- `RelationshipAggregateTests` (14 tests): Constructor, Accept, Reject, Cancel, Remove with valid and invalid state transitions
- `UserBlockAggregateTests` (6 tests): Constructor with valid/invalid inputs, domain event verification

### 10.2 Functional Tests (`SocialService.FunctionalTests`)

Uses `CustomWebApplicationFactory` with InMemoryDatabase.

- `RelationshipsControllerTests` (4 tests): GET friends, mutual friends, suggestions, health check
- `BlocksControllerTests` (2 tests): GET blocked users, with pagination
- `AdminControllerTests` (6 tests): GET all relationships (with/without filter), GET by ID (404), DELETE relationship (404), GET all blocks, DELETE block (404), GET statistics

---

## 11. Known Gaps and Notes

1. **No Authentication**: JWT config exists in settings but authentication middleware is not wired up. All endpoints are public.
2. **No FluentValidation Validators**: The `ValidatorBehavior` is in the pipeline but no `AbstractValidator<T>` classes exist for any command.
3. **No UserProfile CRUD API**: The `UserProfile` entity and repository exist, but there are no controllers or commands to create/update profiles. The `GetFriendsQuery` and other queries reference UserProfile for display names, but profiles must be populated externally.
4. **No IAM Integration Event Consumer**: `UserProfile.UpdateFromEvent()` method exists but no RabbitMQ consumer or integration event handler is implemented.
5. **No Redis Usage**: Redis package is referenced and health check is configured, but no caching logic exists.
6. **UserUnblockedDomainEvent**: Defined but never raised -- the `UnblockUserCommandHandler` does not add this domain event before removing the block.
7. **SampleDomainException**: Leftover from template, unused.
8. **Idempotency**: `IRequestManager`/`RequestManager` and `ClientRequest` are registered but not used by any command handler.
9. **CORS**: Configured to allow any origin/method/header (development-only concern).
10. **Swagger**: Only enabled in Development environment.
