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

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

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

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. Raises MemberExperienceAddedDomainEvent and optionally MemberLevelUpDomainEvent.
  • 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(). Raises StampAddedDomainEvent, and StampCardCompletedDomainEvent if 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). Raises RewardClaimedDomainEvent.
  • 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 name and phone from 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 + MemberLevelUpDomainEvent
  • UpdateGender(gender) -- raises MemberUpdatedDomainEvent
  • UpdateCountryCode(countryCode) -- raises MemberUpdatedDomainEvent
  • UpdatePreferences(preferencesJson) -- raises MemberUpdatedDomainEvent
  • Delete() -- soft delete
  • Restore() -- 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, StampCardCompletedDomainEvent
  • ClaimReward() -- validates completed + not yet claimed. Raises RewardClaimedDomainEvent
  • Reset() -- 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

  1. Serilog request logging
  2. ProblemDetails (RFC 7807)
  3. Swagger (Development only)
  4. CORS (AllowAnyOrigin/Method/Header)
  5. Routing
  6. Authentication (JWT Bearer)
  7. Authorization
  8. Health check endpoints
  9. Controller mapping

MediatR Pipeline Behaviors

  1. LoggingBehavior<,> -- Request/response logging
  2. ValidatorBehavior<,> -- FluentValidation in pipeline
  3. TransactionBehavior<,> -- Auto-wraps commands in DB transactions

DI Registrations

  • MembershipServiceContext (DbContext + IUnitOfWork)
  • IMemberRepository -> MemberRepository
  • ILevelDefinitionRepository -> LevelDefinitionRepository
  • IExperienceTransactionRepository -> ExperienceTransactionRepository
  • IStampCardRepository -> StampCardRepository
  • IRequestManager -> RequestManager
  • IIamServiceClient -> 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 behavior
  • Domain/LevelDefinitionAggregateTests.cs -- LevelDefinition entity behavior
  • Domain/ExperienceTransactionTests.cs -- ExperienceTransaction entity
  • Domain/MembershipLevelTests.cs -- MembershipLevel enumeration
  • Handlers/CreateLevelDefinitionCommandHandlerTests.cs
  • Handlers/GetMemberProgressQueryHandlerTests.cs
  • Handlers/UpdateLevelDefinitionCommandHandlerTests.cs
  • Handlers/DeactivateLevelDefinitionCommandHandlerTests.cs
  • Handlers/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

  1. 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.

  2. Dual Leveling Systems: The service has two level concepts:

    • MembershipLevel (Enumeration: Free/Basic/Premium) -- legacy/unused, stored in membership_levels table
    • LevelDefinition (configurable admin-managed levels with EXP thresholds) -- active system used for level progression
  3. Experience Source as Enumeration: ExperienceSource uses the Enumeration pattern (not stored as EF entity). The source_id integer is persisted; the source object is resolved in-memory via FromValue<>() in the repository layer.

  4. StampCard Active Card Logic: GetActiveCardAsync returns the most recent card where reward_claimed = false (either in-progress or completed-but-unclaimed). After reward is claimed and card is reset, it becomes active again.

  5. Response Format Inconsistency: MembersController returns raw DTOs; StampCardsController wraps responses in { success, data } format. LevelsController returns raw DTOs.

  6. Missing Validators: AddExperienceCommand, CreateLevelDefinitionCommand, UpdateLevelDefinitionCommand, and DeactivateLevelDefinitionCommand do not have FluentValidation validators (some use DataAnnotations instead).

  7. No RabbitMQ Integration: Domain events are dispatched via MediatR (in-process) before SaveChanges. No cross-service event publishing to RabbitMQ is implemented.

  8. Dapper Registered but Unused: Dapper is included as a dependency but no Dapper-based read queries are implemented; all queries use EF Core.