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>
26 KiB
Membership Service - Service Documentation
Auto-generated from source code audit on 2026-03-13.
Overview
Service: membership-service-net Port: 5003 (Development) Database: membership_service (Neon PostgreSQL) Framework: .NET 10.0, C# 14 Architecture: Clean Architecture + CQRS (MediatR 12.4.1)
The Membership Service manages member profiles, experience-based leveling system, and loyalty stamp cards. It extends user identity from IAM Service with membership-specific data (levels, EXP, preferences, stamp cards). Profile information (avatar, phone, address, DOB) is managed by IAM Service; this service handles level progression, experience tracking, and loyalty mechanics.
Key Features
- Member profile management (linked to IAM user by shared ID)
- Configurable experience-based level system (admin-managed level definitions)
- Experience points tracking with 7 source types (Purchase, Referral, Activity, Promotion, Review, CheckIn, Admin)
- Automatic level-up when EXP thresholds are met
- Loyalty stamp cards (buy-N-get-1-free pattern)
- Stamp card lifecycle: create -> collect stamps -> complete -> claim reward -> reset
- IAM Service integration with in-memory caching
- Soft delete for members and level definitions
API Endpoints
MembersController
Route: api/v{version:apiVersion}/members | Auth: [Authorize]
| Method | Path | Description | Auth | Request | Response |
|---|---|---|---|---|---|
| GET | /{id:guid} |
Get member by ID | Authorize | Path: id (Guid) |
MemberDto or 404 |
| GET | /me |
Get current user's member profile | Authorize | JWT sub/id claim |
MemberDto or 404 |
| GET | / |
Get paginated members list | Authorize | Query: pageIndex, pageSize, search |
GetMembersResult |
| POST | / |
Create new member | Authorize | Body: CreateMemberCommand |
201 CreateMemberResult or 409 |
| PUT | /{id:guid} |
Update member profile | Authorize | Path: id, Body: UpdateMemberProfileCommand |
UpdateMemberProfileResult or 404 |
| POST | /{id:guid}/experience |
Add experience points | Authorize | Path: id, Body: AddExperienceCommand |
AddExperienceResult or 404 |
| GET | /{id:guid}/progress |
Get member level progress | Authorize | Path: id |
MemberProgressDto or 404 |
| GET | /{id:guid}/experience |
Get experience history | Authorize | Query: pageIndex, pageSize |
ExperienceHistoryResult |
LevelsController
Route: api/v{version:apiVersion}/levels | Auth: [Authorize]
| Method | Path | Description | Auth | Request | Response |
|---|---|---|---|---|---|
| GET | / |
Get all level definitions | AllowAnonymous | Query: includeInactive (bool) |
IEnumerable<LevelDefinitionDto> |
| POST | / |
Create level definition | Admin role | Body: CreateLevelDefinitionCommand |
201 CreateLevelDefinitionResult or 409 |
| PUT | /{id:guid} |
Update level definition | Admin role | Path: id, Body: UpdateLevelDefinitionCommand |
UpdateLevelDefinitionResult or 404 |
| DELETE | /{id:guid} |
Deactivate level definition (soft delete) | Admin role | Path: id |
204 or 404 |
StampCardsController
Route: api/v{version:apiVersion}/members/stamp-cards | Auth: [Authorize]
| Method | Path | Description | Auth | Request | Response |
|---|---|---|---|---|---|
| GET | / |
Get stamp card for member at shop | Authorize | Query: shopId, memberId |
{success, data: StampCardDto} or 404 |
| GET | /shop/{shopId:guid} |
Get all stamp cards for shop (admin) | Authorize | Path: shopId, Query: pageIndex, pageSize |
{success, data: GetStampCardsResult} |
| POST | / |
Create new stamp card | Authorize | Body: CreateStampCardCommand |
201 {success, data: CreateStampCardResult} or 409 |
| POST | /stamp |
Add stamp (auto-creates card if needed) | Authorize | Body: AddStampCommand |
{success, data: AddStampResult} |
| POST | /{id:guid}/claim |
Claim reward on completed card | Authorize | Path: id |
{success, data: ClaimRewardResult} or 400/404 |
| POST | /{id:guid}/reset |
Reset card for new round | Authorize | Path: id |
{success, data: ResetStampCardResult} or 400/404 |
Health Endpoints
| Path | Description |
|---|---|
/health |
Full health check (PostgreSQL) |
/health/live |
Liveness probe (app running) |
/health/ready |
Readiness probe (dependencies ready) |
Commands
CreateMemberCommand
- Input:
UserId(Guid, required),CountryCode(string, default "VN"),Gender(string?, optional),Name(string?, optional),Phone(string?, optional) - Logic: Checks for existing member by userId. Creates Member entity with level=1, exp=0. Stores name/phone in preferences JSON if provided. Raises
MemberCreatedDomainEvent. - Validator:
CreateMemberCommandValidator-- UserId NotEmpty, CountryCode NotEmpty + Length(2), Gender Must be Male/Female/Other
UpdateMemberProfileCommand
- Input:
MemberId(Guid),Gender(string?),Preferences(string?, JSON),CountryCode(string?) - Logic: Fetches member, applies partial updates via domain methods (UpdateGender, UpdateCountryCode, UpdatePreferences). Raises
MemberUpdatedDomainEvent. - Validator:
UpdateMemberProfileCommandValidator-- MemberId NotEmpty, Gender must be valid, CountryCode Length(2), Preferences must be valid JSON
AddExperienceCommand
- Input:
MemberId(Guid),Points(int),SourceId(int, 1-7),ReferenceId(string?),Metadata(string?) - Logic: Gets member and active level rules. Calls
member.AddExperience()which creates ExperienceTransaction, updates EXP, auto-levels-up if threshold met. Saves transaction + member. RaisesMemberExperienceAddedDomainEventand optionallyMemberLevelUpDomainEvent. - Validator: None (no dedicated validator file)
CreateLevelDefinitionCommand
- Input:
LevelNumber(int, 1-100),Name(string, 1-100 chars),RequiredExp(int, >=0),Description(string?, 500 max),IconUrl(string?, 500 max),BadgeColor(string?, hex format) - Logic: Checks level number uniqueness, creates LevelDefinition entity.
- Validator: None (uses DataAnnotations on command class)
UpdateLevelDefinitionCommand
- Input:
Id(Guid),Name(string?),RequiredExp(int?),Description(string?),IconUrl(string?),BadgeColor(string?),ClearDescription/ClearIconUrl/ClearBadgeColor(bool) - Logic: Fetches level definition, applies partial updates via domain methods. Supports clearing nullable fields.
- Validator: None (uses DataAnnotations)
DeactivateLevelDefinitionCommand
- Input:
Id(Guid) - Logic: Fetches level definition, calls
Deactivate()(sets IsActive=false, soft delete). - Validator: None
CreateStampCardCommand
- Input:
ShopId(Guid),MemberId(Guid),CardName(string),TotalStampsRequired(int, default 10) - Logic: Checks for existing active card at shop for member. Creates new StampCard. Raises
StampCardCreatedDomainEvent. - Validator:
CreateStampCardCommandValidator-- ShopId/MemberId NotEmpty, CardName NotEmpty + MaxLength(200), TotalStampsRequired 1-100
AddStampCommand
- Input:
ShopId(Guid),MemberId(Guid),OrderId(Guid?, optional) - Logic: Finds active card for member+shop. Auto-creates card (10 stamps, "Loyalty Card") if none exists. Calls
stampCard.AddStamp(). RaisesStampAddedDomainEvent, andStampCardCompletedDomainEventif all stamps collected. - Validator:
AddStampCommandValidator-- ShopId/MemberId NotEmpty
ClaimRewardCommand
- Input:
StampCardId(Guid) - Logic: Finds stamp card, calls
ClaimReward()(domain validates card is completed and reward not already claimed). RaisesRewardClaimedDomainEvent. - Validator:
ClaimRewardCommandValidator-- StampCardId NotEmpty
ResetStampCardCommand
- Input:
StampCardId(Guid) - Logic: Finds stamp card, calls
Reset()(domain validates reward was claimed). Resets stamps to 0, clears completion state. - Validator:
ResetStampCardCommandValidator-- StampCardId NotEmpty
Queries
GetMemberByIdQuery
- Input:
MemberId(Guid) - Logic: Fetches member by ID. Extracts
nameandphonefrom preferences JSON for DisplayName/Phone fields. - Output:
MemberDto(Id, UserId, Gender, CountryCode, Preferences, DisplayName, Phone, CurrentLevel, CurrentExp, TotalExpEarned, CreatedAt, UpdatedAt)
GetMembersQuery
- Input:
PageIndex(int),PageSize(int),SearchTerm(string?) - Logic: Paginated query with search by country code or gender. Ordered by created_at DESC.
- Output:
GetMembersResult(Members, TotalCount, PageIndex, PageSize, TotalPages)
GetMemberProgressQuery
- Input:
MemberId(Guid) - Logic: Fetches member + active level definitions. Calculates current/next level, EXP to next level, progress percentage (0-100%). 100% if at max level.
- Output:
MemberProgressDto(MemberId, CurrentLevel, CurrentLevelName, CurrentExp, TotalExpEarned, ExpToNextLevel, ProgressPercent, NextLevel, NextLevelName, BadgeColor)
GetExperienceHistoryQuery
- Input:
MemberId(Guid),PageIndex(int),PageSize(int, default 20) - Logic: Paginated experience transactions ordered by created_at DESC. Sets ExperienceSource from SourceId.
- Output:
ExperienceHistoryResult(Transactions[], TotalCount, PageIndex, PageSize)
GetLevelDefinitionsQuery
- Input:
IncludeInactive(bool, default false) - Logic: Returns all level definitions ordered by level_number. Includes benefits. Filters by active status unless IncludeInactive=true.
- Output:
IEnumerable<LevelDefinitionDto>(Id, LevelNumber, Name, RequiredExp, Description, IconUrl, BadgeColor, IsActive, Benefits[])
GetStampCardQuery
- Input:
ShopId(Guid),MemberId(Guid) - Logic: Gets most recent stamp card for member at shop.
- Output:
StampCardDto(Id, ShopId, MemberId, CardName, TotalStampsRequired, CurrentStamps, IsCompleted, RewardClaimed, CreatedAt, CompletedAt)
GetStampCardsQuery
- Input:
ShopId(Guid),PageIndex(int),PageSize(int) - Logic: Paginated stamp cards for a shop (admin view), ordered by created_at DESC.
- Output:
GetStampCardsResult(Items[], TotalCount, PageIndex, PageSize)
Domain Model
Member (Aggregate Root)
Table: members
| Field | Type | Description |
|---|---|---|
| Id / UserId | Guid | Primary key, same as IAM User ID (ValueGeneratedNever) |
| _countryCode | string | ISO 3166-1 alpha-2 country code (default "VN") |
| _gender | string? | Male, Female, Other |
| _currentLevel | int | Current level number (default 1) |
| _currentExp | int | Current experience points (default 0) |
| _totalExpEarned | int | Lifetime total EXP earned (default 0) |
| _preferences | string? | JSON (jsonb) user preferences, stores name/phone |
| _createdAt | DateTime | UTC creation timestamp |
| _updatedAt | DateTime | UTC last update timestamp |
| _isDeleted | bool | Soft delete flag (default false, global query filter) |
Behavior Methods:
AddExperience(points, source, levelRules, referenceId?, metadata?)-> ExperienceTransaction -- adds EXP, auto-levels-up, creates transaction, raises MemberExperienceAddedDomainEvent + MemberLevelUpDomainEventUpdateGender(gender)-- raises MemberUpdatedDomainEventUpdateCountryCode(countryCode)-- raises MemberUpdatedDomainEventUpdatePreferences(preferencesJson)-- raises MemberUpdatedDomainEventDelete()-- soft deleteRestore()-- undo soft delete
Domain Events: MemberCreatedDomainEvent, MemberExperienceAddedDomainEvent, MemberLevelUpDomainEvent, MemberUpdatedDomainEvent
LevelDefinition (Aggregate Root)
Table: level_definitions
| Field | Type | Description |
|---|---|---|
| Id | Guid | Primary key (ValueGeneratedNever) |
| _levelNumber | int | Unique level number (1, 2, 3...) |
| _name | string | Level name (Bronze, Silver, Gold..., max 100) |
| _requiredExp | int | EXP threshold to reach this level (default 0) |
| _description | string? | Level description |
| _iconUrl | string? | Badge icon URL (max 500) |
| _badgeColor | string? | Hex color for badge (max 20) |
| _isActive | bool | Active status (default true) |
| _createdAt | DateTime | UTC creation timestamp |
| _updatedAt | DateTime | UTC last update timestamp |
Behavior Methods:
UpdateName(name),UpdateRequiredExp(requiredExp),UpdateDescription(description?),UpdateIconUrl(iconUrl?),UpdateBadgeColor(badgeColor?)AddBenefit(benefit),RemoveBenefit(benefit)Activate(),Deactivate()(soft delete)
Relationship: HasMany LevelBenefit (cascade delete)
LevelBenefit (Entity, owned by LevelDefinition)
Table: level_benefits
| Field | Type | Description |
|---|---|---|
| Id | Guid | Primary key (ValueGeneratedNever) |
| _levelDefinitionId | Guid | FK to level_definitions |
| _benefitType | string | Benefit type (discount_percent, free_shipping, etc., max 50) |
| _benefitValue | string | JSON value (jsonb) |
| _description | string? | Benefit description |
| _isActive | bool | Active status (default true) |
| _createdAt | DateTime | UTC creation timestamp |
Behavior Methods: UpdateBenefitValue(value), UpdateDescription(description?), Activate(), Deactivate()
ExperienceTransaction (Entity)
Table: experience_transactions
| Field | Type | Description |
|---|---|---|
| Id | Guid | Primary key (ValueGeneratedNever) |
| _memberId | Guid | FK to members |
| _points | int | EXP points earned |
| _sourceId | int | Source enum ID (1-7) |
| _referenceId | string? | Reference ID (order, referral code, max 100) |
| _metadata | string? | Additional metadata (jsonb) |
| _levelAtTime | int | Member's level when EXP was earned |
| _createdAt | DateTime | UTC creation timestamp |
Note: Source navigation property is ignored by EF (Enumeration pattern), resolved via ExperienceSource.FromValue<>() in repository.
ExperienceSource (Enumeration)
Not persisted as table. Stored as source_id integer in experience_transactions.
| Id | Name | Description |
|---|---|---|
| 1 | Purchase | Order/purchase completion |
| 2 | Referral | Friend referral |
| 3 | Activity | App browsing/engagement |
| 4 | Promotion | Promotional campaigns |
| 5 | Review | Product reviews |
| 6 | CheckIn | Daily check-in |
| 7 | Admin | Manually granted by admin |
MembershipLevel (Enumeration)
Table: membership_levels (lookup table)
| Id | Name |
|---|---|
| 1 | Free |
| 2 | Basic |
| 3 | Premium |
Note: This enumeration is stored as a DB table but not actively used in current command/query logic. The actual leveling system uses the configurable LevelDefinition aggregate.
StampCard (Aggregate Root)
Table: stamp_cards
| Field | Type | Description |
|---|---|---|
| Id | Guid | Primary key |
| _shopId | Guid | Shop that issued the card |
| _memberId | Guid | Member who owns the card |
| _cardName | string | Display name (max 200) |
| _totalStampsRequired | int | Stamps needed for reward (default 10) |
| _currentStamps | int | Stamps collected so far (default 0) |
| _isCompleted | bool | All stamps collected (default false) |
| _rewardClaimed | bool | Reward claimed after completion (default false) |
| _createdAt | DateTime | UTC creation timestamp |
| _completedAt | DateTime? | UTC completion timestamp |
Behavior Methods:
AddStamp()-- increments stamps, marks completed when threshold met. Raises StampAddedDomainEvent, StampCardCompletedDomainEventClaimReward()-- validates completed + not yet claimed. Raises RewardClaimedDomainEventReset()-- validates reward was claimed, resets all state for new round
Domain Events: StampCardCreatedDomainEvent, StampAddedDomainEvent, StampCardCompletedDomainEvent, RewardClaimedDomainEvent
Database Schema
Tables
members
| Column | Type | Constraints |
|---|---|---|
| id | uuid | PK, ValueGeneratedNever |
| country_code | varchar(2) | NOT NULL |
| gender | varchar(10) | nullable |
| current_level | int | NOT NULL, default 1 |
| current_exp | int | NOT NULL, default 0 |
| total_exp_earned | int | NOT NULL, default 0 |
| preferences | jsonb | nullable |
| created_at | timestamp | NOT NULL |
| updated_at | timestamp | NOT NULL |
| is_deleted | bool | NOT NULL, default false |
Indexes: ix_members_created_at, ix_members_current_level, ix_members_current_exp, ix_members_is_deleted
Global Query Filter: WHERE is_deleted = false
level_definitions
| Column | Type | Constraints |
|---|---|---|
| id | uuid | PK, ValueGeneratedNever |
| level_number | int | NOT NULL, UNIQUE |
| name | varchar(100) | NOT NULL |
| required_exp | int | NOT NULL, default 0 |
| description | text | nullable |
| icon_url | varchar(500) | nullable |
| badge_color | varchar(20) | nullable |
| is_active | bool | NOT NULL, default true |
| created_at | timestamp | NOT NULL |
| updated_at | timestamp | NOT NULL |
Indexes: ix_level_definitions_level_number (unique), ix_level_definitions_is_active
level_benefits
| Column | Type | Constraints |
|---|---|---|
| id | uuid | PK, ValueGeneratedNever |
| level_definition_id | uuid | NOT NULL, FK -> level_definitions (cascade) |
| benefit_type | varchar(50) | NOT NULL |
| benefit_value | jsonb | NOT NULL |
| description | text | nullable |
| is_active | bool | NOT NULL, default true |
| created_at | timestamp | NOT NULL |
Indexes: ix_level_benefits_level_definition_id
experience_transactions
| Column | Type | Constraints |
|---|---|---|
| id | uuid | PK, ValueGeneratedNever |
| member_id | uuid | NOT NULL |
| points | int | NOT NULL |
| source_id | int | NOT NULL |
| reference_id | varchar(100) | nullable |
| metadata | jsonb | nullable |
| level_at_time | int | NOT NULL |
| created_at | timestamp | NOT NULL |
Indexes: ix_experience_transactions_member_id, ix_experience_transactions_source, ix_experience_transactions_created_at
membership_levels
| Column | Type | Constraints |
|---|---|---|
| id | int | PK, ValueGeneratedNever |
| name | varchar(50) | NOT NULL |
Note: Lookup table for MembershipLevel enumeration (Free=1, Basic=2, Premium=3). Not actively used in current business logic.
stamp_cards
| Column | Type | Constraints |
|---|---|---|
| id | uuid | PK |
| shop_id | uuid | NOT NULL |
| member_id | uuid | NOT NULL |
| card_name | varchar(200) | NOT NULL |
| total_stamps_required | int | NOT NULL, default 10 |
| current_stamps | int | NOT NULL, default 0 |
| is_completed | bool | NOT NULL, default false |
| reward_claimed | bool | NOT NULL, default false |
| created_at | timestamp | NOT NULL |
| completed_at | timestamp | nullable |
Indexes: ix_stamp_cards_shop_member (composite: shop_id + member_id), ix_stamp_cards_shop_id, ix_stamp_cards_is_completed, ix_stamp_cards_created_at
Migration History
20260115105939_AddLevelAndExperienceSystem-- Initial migration with all tables
Dependencies
NuGet Packages
API Layer:
| Package | Version |
|---|---|
| MediatR | 12.4.1 |
| FluentValidation | 11.11.0 |
| FluentValidation.DependencyInjectionExtensions | 11.11.0 |
| Microsoft.EntityFrameworkCore.Design | 10.0.2 |
| Swashbuckle.AspNetCore | 7.2.0 |
| Swashbuckle.AspNetCore.Annotations | 7.2.0 |
| Asp.Versioning.Mvc | 8.1.0 |
| Asp.Versioning.Mvc.ApiExplorer | 8.1.0 |
| AspNetCore.HealthChecks.NpgSql | 8.0.2 |
| AspNetCore.HealthChecks.Redis | 8.0.1 |
| Hellang.Middleware.ProblemDetails | 6.5.1 |
| Serilog.AspNetCore | 8.0.3 |
| Serilog.Sinks.Console | 6.0.0 |
| Serilog.Sinks.Seq | 8.0.0 |
| Microsoft.AspNetCore.Authentication.JwtBearer | 10.0.0 |
Domain Layer:
| Package | Version |
|---|---|
| MediatR.Contracts | 2.0.1 |
Infrastructure Layer:
| 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 |
| Microsoft.Extensions.Http | 9.0.0 |
Service Dependencies
- IAM Service: User validation, roles, permissions (via HttpIamServiceClient with in-memory caching)
- PostgreSQL: Primary data store (Neon PostgreSQL, connection with retry policy - 5 retries, 30s max delay)
- Redis: Configured in appsettings but not actively used in repositories (StackExchange.Redis package available)
Configuration
appsettings.json
ConnectionStrings:DefaultConnection -- PostgreSQL connection string (also supports DATABASE_URL env var)
Redis:Host/Port/Password/Database -- Redis connection (configured but not actively used in code)
Jwt:Secret/Issuer/Audience -- JWT validation settings
Jwt:Authority -- OIDC authority URL (default: http://localhost:5001)
IamService:BaseUrl -- IAM Service base URL (default: http://iam-service-net:8080)
IamService:ServiceName -- Internal service name header (default: membership-service)
IamService:TimeoutSeconds -- HTTP client timeout (default: 30)
IamService:CacheDurationSeconds -- User info cache TTL (default: 300 = 5min)
IamService:HealthCheckCacheDurationSeconds -- Health check cache TTL (default: 60 = 1min)
Middleware Pipeline
- Serilog request logging
- ProblemDetails (RFC 7807)
- Swagger (Development only)
- CORS (AllowAnyOrigin/Method/Header)
- Routing
- Authentication (JWT Bearer)
- Authorization
- Health check endpoints
- Controller mapping
MediatR Pipeline Behaviors
LoggingBehavior<,>-- Request/response loggingValidatorBehavior<,>-- FluentValidation in pipelineTransactionBehavior<,>-- Auto-wraps commands in DB transactions
DI Registrations
MembershipServiceContext(DbContext + IUnitOfWork)IMemberRepository->MemberRepositoryILevelDefinitionRepository->LevelDefinitionRepositoryIExperienceTransactionRepository->ExperienceTransactionRepositoryIStampCardRepository->StampCardRepositoryIRequestManager->RequestManagerIIamServiceClient->HttpIamServiceClient(HttpClient factory)
Auto-Migration
The service auto-applies EF Core migrations on startup. Migration failures are logged but do not crash the application.
Tests
Unit Tests (tests/MembershipService.UnitTests/)
Domain/MemberAggregateTests.cs-- Member entity behaviorDomain/LevelDefinitionAggregateTests.cs-- LevelDefinition entity behaviorDomain/ExperienceTransactionTests.cs-- ExperienceTransaction entityDomain/MembershipLevelTests.cs-- MembershipLevel enumerationHandlers/CreateLevelDefinitionCommandHandlerTests.csHandlers/GetMemberProgressQueryHandlerTests.csHandlers/UpdateLevelDefinitionCommandHandlerTests.csHandlers/DeactivateLevelDefinitionCommandHandlerTests.csHandlers/AddExperienceCommandHandlerTests.cs
Functional Tests (tests/MembershipService.FunctionalTests/)
CustomWebApplicationFactory.cs-- Test setup with in-memory DB
Domain Events Summary
| Event | Raised By | Trigger |
|---|---|---|
| MemberCreatedDomainEvent | Member constructor | New member created |
| MemberUpdatedDomainEvent | Member.UpdateGender/CountryCode/Preferences | Profile updated |
| MemberExperienceAddedDomainEvent | Member.AddExperience | EXP added |
| MemberLevelUpDomainEvent | Member.AddExperience | Level threshold crossed |
| MembershipLevelChangedDomainEvent | (defined but not currently raised in code) | Membership tier change |
| StampCardCreatedDomainEvent | StampCard constructor | New stamp card created |
| StampAddedDomainEvent | StampCard.AddStamp | Stamp added to card |
| StampCardCompletedDomainEvent | StampCard.AddStamp | All stamps collected |
| RewardClaimedDomainEvent | StampCard.ClaimReward | Reward claimed |
Architecture Notes
-
Member.Id == IAM UserId: Member entity uses the IAM Service's user ID as its primary key (ValueGeneratedNever). This ensures a 1:1 mapping without a separate FK.
-
Dual Leveling Systems: The service has two level concepts:
MembershipLevel(Enumeration: Free/Basic/Premium) -- legacy/unused, stored inmembership_levelstableLevelDefinition(configurable admin-managed levels with EXP thresholds) -- active system used for level progression
-
Experience Source as Enumeration: ExperienceSource uses the Enumeration pattern (not stored as EF entity). The
source_idinteger is persisted; the source object is resolved in-memory viaFromValue<>()in the repository layer. -
StampCard Active Card Logic:
GetActiveCardAsyncreturns the most recent card wherereward_claimed = false(either in-progress or completed-but-unclaimed). After reward is claimed and card is reset, it becomes active again. -
Response Format Inconsistency: MembersController returns raw DTOs; StampCardsController wraps responses in
{ success, data }format. LevelsController returns raw DTOs. -
Missing Validators:
AddExperienceCommand,CreateLevelDefinitionCommand,UpdateLevelDefinitionCommand, andDeactivateLevelDefinitionCommanddo not have FluentValidation validators (some use DataAnnotations instead). -
No RabbitMQ Integration: Domain events are dispatched via MediatR (in-process) before SaveChanges. No cross-service event publishing to RabbitMQ is implemented.
-
Dapper Registered but Unused: Dapper is included as a dependency but no Dapper-based read queries are implemented; all queries use EF Core.