27 KiB
PromotionService - Service Documentation
Generated from actual source code audit on 2026-03-13.
1. Overview
Purpose: Manages marketing campaigns with vouchers for merchants. Supports campaign lifecycle (draft, active, paused, completed, cancelled), voucher generation/claiming/redemption, and escrow integration with the Wallet Service.
Port: 5008 (local development via launchSettings.json), 8080 (Docker container)
Database: PostgreSQL - promotion_service on Neon (cloud)
- Connection string configured via
ConnectionStrings:DefaultConnectionorDATABASE_URLenv var - Cloud host:
ep-holy-glitter-a4hongg7-pooler.us-east-1.aws.neon.tech
Framework: .NET 10.0 (C# 14), Clean Architecture + CQRS (MediatR)
Solution file: PromotionService.slnx
Migration: Single migration 20260117144846_InitialCreate - auto-applied on startup via dbContext.Database.MigrateAsync()
2. API Endpoints
2.1 CampaignsController (api/v1/campaigns)
| Method | Route | Auth | Description |
|---|---|---|---|
| POST | /api/v1/campaigns |
Yes | Create a new campaign |
| GET | /api/v1/campaigns/{id} |
No | Get campaign by ID |
| GET | /api/v1/campaigns?merchantId=&activeOnly= |
No | Get campaigns list (optional filters) |
| POST | /api/v1/campaigns/{id}/activate |
Yes | Activate a campaign |
| POST | /api/v1/campaigns/{id}/pause |
Yes | Pause a campaign |
| POST | /api/v1/campaigns/{id}/cancel |
Yes | Cancel a campaign (releases escrow) |
2.2 VouchersController (api/v1/vouchers)
| Method | Route | Auth | Description |
|---|---|---|---|
| POST | /api/v1/vouchers/claim |
Yes | Claim a free voucher |
| GET | /api/v1/vouchers/validate/{code}?userId= |
Yes | Validate a voucher code for a user |
| POST | /api/v1/vouchers/redeem |
Yes | Redeem a voucher against an order |
| GET | /api/v1/vouchers/user/{userId} |
Yes | Get all vouchers owned by a user |
2.3 AdminCampaignsController (api/v1/admin/campaigns)
All endpoints require [Authorize].
| Method | Route | Auth | Description |
|---|---|---|---|
| GET | /api/v1/admin/campaigns?pageNumber=&pageSize=&merchantId=&status=&searchTerm= |
Yes | Paginated campaign list with filters |
| GET | /api/v1/admin/campaigns/{id}/statistics |
Yes | Campaign statistics (voucher counts, redemption totals, utilization rate) |
| GET | /api/v1/admin/campaigns/{id}/vouchers?pageNumber=&pageSize=&status= |
Yes | Paginated vouchers for a campaign |
| PUT | /api/v1/admin/campaigns/{id} |
Yes | Update campaign details (name, description, dates, maxPerUser) |
| POST | /api/v1/admin/campaigns/{id}/complete |
Yes | Force complete a campaign |
| DELETE | /api/v1/admin/campaigns/{id} |
Yes | Soft delete (cancels) a campaign |
2.4 AdminVouchersController (api/v1/admin/vouchers)
All endpoints require [Authorize].
| Method | Route | Auth | Description |
|---|---|---|---|
| GET | /api/v1/admin/vouchers?pageNumber=&pageSize=&campaignId=&userId=&status=&codeSearch= |
Yes | Paginated voucher list with filters |
| GET | /api/v1/admin/vouchers/search?q= |
Yes | Search vouchers by code (NOTE: handler not implemented) |
| GET | /api/v1/admin/vouchers/by-user/{userId}?pageNumber=&pageSize= |
Yes | Vouchers by user |
| POST | /api/v1/admin/vouchers/{id}/revoke |
Yes | Revoke a voucher (marks as expired) |
| POST | /api/v1/admin/vouchers/{id}/extend |
Yes | Extend voucher expiry by N days |
2.5 AdminRedemptionsController (api/v1/admin/redemptions)
All endpoints require [Authorize].
| Method | Route | Auth | Description |
|---|---|---|---|
| GET | /api/v1/admin/redemptions?pageNumber=&pageSize=&campaignId=&voucherId=&userId=&dateFrom=&dateTo= |
Yes | Paginated redemption list with filters |
| GET | /api/v1/admin/redemptions/by-campaign/{campaignId}?pageNumber=&pageSize= |
Yes | Redemptions by campaign |
| GET | /api/v1/admin/redemptions/by-voucher/{voucherId}?pageNumber=&pageSize= |
Yes | Redemptions by voucher |
| GET | /api/v1/admin/redemptions/statistics?campaignId= |
Yes | Redemption statistics (totals, averages, today/week/month counts) |
2.6 Health Check Endpoints
| Route | Description |
|---|---|
/health |
Full health check (PostgreSQL + Wallet Service) |
/health/live |
Liveness probe (always passes if app is running) |
/health/ready |
Readiness probe (checks all dependencies) |
3. Commands
3.1 Campaign Commands
CreateCampaignCommand
- Parameters: MerchantId (Guid), MerchantWalletId (Guid), Name (string), Description (string?), BackingAssetType (string: "Point"/"Currency"), BackingAssetCode (string), FaceValue (decimal), AcquisitionType (string: "Free"/"ExchangePoints"/"Purchase"), AcquisitionPrice (decimal), TotalVouchers (int), StartDate (DateTime), EndDate (DateTime), VoucherValidityDays (int, default 30), MaxPerUser (int, default 1)
- Returns: CampaignDto
- Handler: CreateCampaignCommandHandler
- Logic: Parses enums, creates Campaign aggregate, optionally creates escrow hold via Wallet Service, generates voucher codes, persists. Raises
CampaignCreatedDomainEvent.
ActivateCampaignCommand
- Parameters: CampaignId (Guid)
- Returns: bool
- Handler: ActivateCampaignCommandHandler
- Logic: Finds campaign, calls
campaign.Activate(). Requires escrow to be set. RaisesCampaignActivatedDomainEvent.
PauseCampaignCommand
- Parameters: CampaignId (Guid)
- Returns: bool
- Handler: PauseCampaignCommandHandler
- Logic: Finds campaign, calls
campaign.Pause(). Only works on Active campaigns.
CancelCampaignCommand
- Parameters: CampaignId (Guid)
- Returns: bool
- Handler: CancelCampaignCommandHandler
- Logic: Releases escrow hold via Wallet Service if exists, then calls
campaign.Cancel(). RaisesCampaignCancelledDomainEvent.
UpdateCampaignCommand (Admin)
- Parameters: CampaignId (Guid), Name (string?), Description (string?), StartDate (DateTime?), EndDate (DateTime?), MaxPerUser (int?)
- Returns: bool
- Handler: UpdateCampaignCommandHandler
CompleteCampaignCommand (Admin)
- Parameters: CampaignId (Guid)
- Returns: bool
- Handler: CompleteCampaignCommandHandler
- Logic: Force completes campaign. Only from Active or Paused state.
DeleteCampaignCommand (Admin)
- Parameters: CampaignId (Guid)
- Returns: bool
- Handler: DeleteCampaignCommandHandler
- Logic: Soft delete implemented as Cancel().
3.2 Voucher Commands
ClaimVoucherCommand
- Parameters: CampaignId (Guid), UserId (Guid)
- Returns: VoucherDto
- Handler: ClaimVoucherCommandHandler
- Logic: Verifies campaign is Free acquisition type, calls
campaign.IssueVoucher(), persists. RaisesVoucherClaimedDomainEvent.
ExchangeVoucherCommand
- Parameters: CampaignId (Guid), UserId (Guid), UserWalletId (Guid)
- Returns: VoucherDto
- Handler: NOT IMPLEMENTED (command defined, no handler)
PurchaseVoucherCommand
- Parameters: CampaignId (Guid), UserId (Guid), UserWalletId (Guid)
- Returns: VoucherDto
- Handler: NOT IMPLEMENTED (command defined, no handler)
RedeemVoucherCommand
- Parameters: VoucherCode (string), UserId (Guid), OrderId (Guid?), OrderAmount (decimal)
- Returns: RedemptionDto
- Handler: RedeemVoucherCommandHandler
- Logic: Finds voucher by code, verifies ownership and validity, calculates amounts (min of order amount and voucher value), executes escrow via Wallet Service, creates Redemption record. Surplus refunded to merchant.
RevokeVoucherCommand (Admin)
- Parameters: VoucherId (Guid), Reason (string)
- Returns: bool
- Handler: RevokeVoucherCommandHandler
- Logic: Marks voucher as Expired (revoke = expire).
ExtendVoucherExpiryCommand (Admin)
- Parameters: VoucherId (Guid), AdditionalDays (int)
- Returns: bool
- Handler: ExtendVoucherExpiryCommandHandler
4. Queries
4.1 Public Queries
GetCampaignQuery
- Parameters: CampaignId (Guid)
- Returns: CampaignDto? (null if not found)
- Handler: GetCampaignQueryHandler
GetCampaignsQuery
- Parameters: MerchantId (Guid?), ActiveOnly (bool)
- Returns: IEnumerable<CampaignSummaryDto>
- Handler: GetCampaignsQueryHandler
- Logic: If ActiveOnly=true, returns active campaigns within date range. If MerchantId provided, returns by merchant. Otherwise defaults to active campaigns.
GetCampaignStatisticsQuery
- Parameters: CampaignId (Guid)
- Returns: CampaignStatisticsDto?
- Handler: NOT IMPLEMENTED (query defined, no handler)
ValidateVoucherQuery
- Parameters: VoucherCode (string), UserId (Guid)
- Returns: VoucherValidationDto
- Handler: ValidateVoucherQueryHandler
- Logic: Checks voucher exists, ownership matches, and is valid for redemption. Returns validation result with remaining value and campaign name.
GetUserVouchersQuery
- Parameters: UserId (Guid)
- Returns: IEnumerable<VoucherSummaryDto>
- Handler: GetUserVouchersQueryHandler
4.2 Admin Queries
GetAllCampaignsQuery
- Parameters: PageNumber (int), PageSize (int), MerchantId (Guid?), Status (string?), SearchTerm (string?)
- Returns: PaginatedResponse<AdminCampaignListDto>
- Handler: GetAllCampaignsQueryHandler (uses PromotionServiceContext directly)
GetAdminCampaignStatisticsQuery
- Parameters: CampaignId (Guid)
- Returns: AdminCampaignStatisticsDto?
- Handler: GetAdminCampaignStatisticsQueryHandler
- Logic: Loads campaign with vouchers, loads redemptions, calculates claimed/redeemed/expired counts, total redeemed/refunded values, utilization rate.
GetCampaignVouchersQuery
- Parameters: CampaignId (Guid), PageNumber (int), PageSize (int), Status (string?)
- Returns: PaginatedResponse<AdminVoucherListDto>
- Handler: NOT IMPLEMENTED (query defined, no handler -- uses GetAllVouchersQueryHandler with CampaignId filter via controller)
GetAllVouchersQuery
- Parameters: PageNumber (int), PageSize (int), CampaignId (Guid?), UserId (Guid?), Status (string?), CodeSearch (string?)
- Returns: PaginatedResponse<AdminVoucherListDto>
- Handler: GetAllVouchersQueryHandler (uses join with Campaigns for CampaignName)
SearchVouchersQuery
- Parameters: SearchTerm (string)
- Returns: IEnumerable<AdminVoucherListDto>
- Handler: NOT IMPLEMENTED (query defined, no handler)
GetAllRedemptionsQuery
- Parameters: PageNumber (int), PageSize (int), CampaignId (Guid?), VoucherId (Guid?), UserId (Guid?), DateFrom (DateTime?), DateTo (DateTime?)
- Returns: PaginatedResponse<AdminRedemptionListDto>
- Handler: GetAllRedemptionsQueryHandler (uses double join with Vouchers and Campaigns)
GetRedemptionStatisticsQuery
- Parameters: CampaignId (Guid?)
- Returns: AdminRedemptionStatisticsDto
- Handler: GetRedemptionStatisticsQueryHandler
- Logic: Calculates total count, total amount used/refunded, average amount, and counts for today/this week/this month.
5. Domain Model
5.1 Campaign Aggregate
Campaign (Aggregate Root)
-
File:
src/PromotionService.Domain/AggregatesModel/CampaignAggregate/Campaign.cs -
Extends: Entity, IAggregateRoot
-
Properties:
- MerchantId (Guid) - owning merchant
- Name (string, max 255) - campaign name
- Description (string?, max 1000)
- BackingAssetTypeId (int) / BackingAssetType (AssetType) - Currency(1) or Point(2)
- BackingAssetCode (string, max 10) - e.g., "VND", "USD", "PPoint"
- FaceValue (decimal, 18,2) - value of each voucher
- AcquisitionTypeId (int) / AcquisitionType (AcquisitionType) - Free(1), ExchangePoints(2), Purchase(3)
- AcquisitionPrice (decimal, 18,2) - cost to acquire (0 for free)
- EscrowHoldId (Guid?) - Wallet Service hold reference
- EscrowWalletId (Guid?) - Wallet providing escrow
- EscrowAmount (decimal, 18,2) - total held (FaceValue * TotalVouchers)
- TotalVouchers (int) - total planned vouchers
- IssuedVouchers (int) - count of claimed vouchers
- MaxPerUser (int) - max vouchers per user (0 = unlimited)
- StartDate (DateTime) - campaign start
- EndDate (DateTime) - campaign end
- VoucherValidityDays (int) - days voucher is valid after claiming
- StatusId (int) / Status (CampaignStatus)
- CreatedAt (DateTime)
- UpdatedAt (DateTime)
- Vouchers (IReadOnlyCollection<Voucher>) - child vouchers
-
Behavior Methods:
SetEscrowHold(walletId, holdId)- set escrow references (Draft only)Activate()- Draft/Paused -> Active (requires escrow)Pause()- Active -> PausedCancel()- any except Completed/Cancelled -> CancelledComplete()- Active/Paused -> CompletedUpdate(name?, description?, startDate?, endDate?, maxPerUser?)- update fieldsGenerateVouchers(count)- create voucher codes (Draft only, format: "V" + 6 alphanumeric)IssueVoucher(userId)- claim an available voucher for a user (Active only, enforces MaxPerUser)GetVoucherByCode(code)- find voucher by codeGetUserVouchers(userId)- get user's vouchers- Computed:
AvailableVoucherCount,ClaimedVoucherCount,RedeemedVoucherCount,TotalRedeemedValue
-
Validation (constructor):
- Name cannot be empty
- FaceValue must be > 0
- TotalVouchers must be > 0
- EndDate must be after StartDate
- Non-free campaigns require AcquisitionPrice > 0
Voucher (Entity, child of Campaign)
-
File:
src/PromotionService.Domain/AggregatesModel/CampaignAggregate/Voucher.cs -
Extends: Entity
-
Properties:
- CampaignId (Guid) - parent campaign FK
- Code (string, max 20, unique) - voucher code
- OwnerId (Guid?) - user who claimed (null if unclaimed)
- FaceValue (decimal, 18,2) - original value
- RemainingValue (decimal, 18,2) - value left after partial redemptions
- StatusId (int) / Status (VoucherStatus)
- ClaimedAt (DateTime?) - when claimed
- ExpiresAt (DateTime?) - expiry date (set on claim)
- RedeemedAt (DateTime?) - last redemption time
- CreatedAt (DateTime)
- UpdatedAt (DateTime)
-
Behavior Methods:
Claim(userId, validityDays)- Available -> Claimed, sets owner and expiryRedeem(amount)- deducts from RemainingValue, returns actual amount deducted, status becomes PartiallyRedeemed or FullyRedeemedExpire()- marks as ExpiredExtendExpiry(additionalDays)- extends ExpiresAtIsValidForRedemption()- checks status is Claimed/PartiallyRedeemed, has remaining value, not expiredIsExpired(computed) - checks ExpiresAt vs now
5.2 Redemption Aggregate
Redemption (Aggregate Root)
- File:
src/PromotionService.Domain/AggregatesModel/RedemptionAggregate/Redemption.cs - Extends: Entity, IAggregateRoot
- Properties:
- VoucherId (Guid) - redeemed voucher
- CampaignId (Guid) - campaign reference
- UserId (Guid) - user who redeemed
- OrderId (Guid?) - linked order (optional)
- AmountUsed (decimal, 18,2) - amount applied to order
- AmountRefunded (decimal, 18,2) - surplus returned to merchant
- ExecutionReference (string?, max 100) - Wallet Service execution ref
- RedeemedAt (DateTime) - redemption timestamp
- CreatedAt (DateTime)
5.3 Enumerations (Type-Safe Enum Pattern)
CampaignStatus
| Id | Name | Description |
|---|---|---|
| 1 | Draft | Campaign being prepared |
| 2 | Active | Campaign running, vouchers can be claimed |
| 3 | Paused | Campaign temporarily stopped |
| 4 | Completed | Campaign ended normally |
| 5 | Cancelled | Campaign cancelled, escrow released |
VoucherStatus
| Id | Name | Description |
|---|---|---|
| 1 | Available | Not claimed yet |
| 2 | Claimed | Owned by a user |
| 3 | PartiallyRedeemed | Some value used |
| 4 | FullyRedeemed | All value used |
| 5 | Expired | Expired without full use |
AssetType
| Id | Name | Description |
|---|---|---|
| 1 | Currency | VND, USD, etc. |
| 2 | Point | Loyalty points (PPoint) |
AcquisitionType
| Id | Name | Description |
|---|---|---|
| 1 | Free | Free giveaway |
| 2 | ExchangePoints | Exchange with loyalty points |
| 3 | Purchase | Purchase with currency |
5.4 Domain Exceptions
PromotionDomainException- base for business rule violationsCampaignNotActiveException- operating on non-active campaignVoucherAlreadyClaimedException- claiming an already-claimed voucherVoucherExpiredException- using an expired voucherInsufficientVoucherValueException- voucher has insufficient remaining valueDomainException- generic base (from template)SampleDomainException- leftover from template (unused)
6. Database Schema
6.1 Table: campaigns
| Column | Type | Nullable | Constraints |
|---|---|---|---|
| id | uuid | NO | PK, ValueGeneratedNever |
| merchant_id | uuid | NO | |
| name | varchar(255) | NO | |
| description | varchar(1000) | YES | |
| backing_asset_type_id | integer | NO | |
| backing_asset_code | varchar(10) | NO | |
| face_value | numeric(18,2) | NO | |
| acquisition_type_id | integer | NO | |
| acquisition_price | numeric(18,2) | NO | |
| escrow_hold_id | uuid | YES | |
| escrow_wallet_id | uuid | YES | |
| escrow_amount | numeric(18,2) | NO | |
| total_vouchers | integer | NO | |
| issued_vouchers | integer | NO | |
| max_per_user | integer | NO | |
| start_date | timestamp with time zone | NO | |
| end_date | timestamp with time zone | NO | |
| voucher_validity_days | integer | NO | |
| status_id | integer | NO | |
| created_at | timestamp with time zone | NO | |
| updated_at | timestamp with time zone | NO |
Indexes:
ix_campaigns_merchant_idon (merchant_id)ix_campaigns_status_idon (status_id)ix_campaigns_date_rangeon (start_date, end_date)
6.2 Table: vouchers
| Column | Type | Nullable | Constraints |
|---|---|---|---|
| id | uuid | NO | PK, ValueGeneratedNever |
| campaign_id | uuid | NO | FK -> campaigns.id (CASCADE) |
| code | varchar(20) | NO | |
| owner_id | uuid | YES | |
| face_value | numeric(18,2) | NO | |
| remaining_value | numeric(18,2) | NO | |
| status_id | integer | NO | |
| claimed_at | timestamp with time zone | YES | |
| expires_at | timestamp with time zone | YES | |
| redeemed_at | timestamp with time zone | YES | |
| created_at | timestamp with time zone | NO | |
| updated_at | timestamp with time zone | NO |
Indexes:
ix_vouchers_codeon (code) UNIQUEix_vouchers_owner_idon (owner_id)ix_vouchers_campaign_idon (campaign_id)ix_vouchers_status_idon (status_id)
6.3 Table: redemptions
| Column | Type | Nullable | Constraints |
|---|---|---|---|
| id | uuid | NO | PK, ValueGeneratedNever |
| voucher_id | uuid | NO | |
| campaign_id | uuid | NO | |
| user_id | uuid | NO | |
| order_id | uuid | YES | |
| amount_used | numeric(18,2) | NO | |
| amount_refunded | numeric(18,2) | NO | |
| execution_reference | varchar(100) | YES | |
| redeemed_at | timestamp with time zone | NO | |
| created_at | timestamp with time zone | NO |
Indexes:
ix_redemptions_voucher_idon (voucher_id)ix_redemptions_user_idon (user_id)ix_redemptions_campaign_idon (campaign_id)ix_redemptions_order_idon (order_id)
Note: The redemptions table does NOT have a foreign key to vouchers or campaigns at the database level (no FK constraint defined in EF configuration).
7. Domain Events
All defined in src/PromotionService.Domain/Events/PromotionDomainEvents.cs. These are INotification records dispatched via MediatR before SaveChanges.
| Event | Parameters | Raised By |
|---|---|---|
| CampaignCreatedDomainEvent | CampaignId, MerchantId, CampaignName | Campaign constructor |
| CampaignActivatedDomainEvent | CampaignId, MerchantId | Campaign.Activate() |
| CampaignCancelledDomainEvent | CampaignId, MerchantId, EscrowHoldId? | Campaign.Cancel() |
| VoucherClaimedDomainEvent | VoucherId, CampaignId, UserId, VoucherCode | Campaign.IssueVoucher() |
| VoucherRedeemedDomainEvent | VoucherId, CampaignId, UserId, AmountUsed, AmountRefunded | Defined but NOT raised by any code |
Note: No domain event handlers are implemented in the service. Events are dispatched via PromotionServiceContext.DispatchDomainEventsAsync() but no INotificationHandler<T> implementations exist. These events are available for future cross-service integration (e.g., via RabbitMQ integration events).
8. Dependencies
8.1 NuGet Packages
API Layer (PromotionService.API):
- MediatR 12.4.1
- FluentValidation 11.11.0
- FluentValidation.DependencyInjectionExtensions 11.11.0
- Microsoft.AspNetCore.Authentication.JwtBearer 10.0.0
- Microsoft.EntityFrameworkCore.Design 10.0.0
- 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
Domain Layer (PromotionService.Domain):
- MediatR.Contracts 2.0.1 (for INotification only)
Infrastructure Layer (PromotionService.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
Build (Directory.Build.props):
- Microsoft.SourceLink.GitHub 8.0.0
8.2 External Service Dependencies
| Service | Purpose | Base URL (Docker) | Base URL (Dev) |
|---|---|---|---|
| Wallet Service | Escrow holds (create, execute, release, cancel) | http://wallet-service-net:8080 | http://localhost:5003 |
| IAM Service | JWT authentication (Authority for token validation) | http://iam-service-net:8080 | http://localhost:5001 |
8.3 Wallet Service Client (IWalletServiceClient)
HTTP client with Polly retry policy (3 retries, exponential backoff). Methods:
CreateHoldAsync- POST/api/v1/wallets/{walletId}/holdsExecuteHoldAsync- POST/api/v1/wallets/{walletId}/holds/{holdId}/executeReleaseHoldAsync- POST/api/v1/wallets/{walletId}/holds/{holdId}/releaseCancelHoldAsync- POST/api/v1/wallets/{walletId}/holds/{holdId}/cancelGetWalletByUserIdAsync- GET/api/v1/wallets/user/{userId}
9. Configuration
9.1 appsettings.json
ConnectionStrings:DefaultConnection - PostgreSQL connection string (or DATABASE_URL env var)
WalletService:BaseUrl - Wallet service URL (default: http://wallet-service-net:8080)
WalletService:TimeoutSeconds - HTTP timeout (30s)
IamService:BaseUrl - IAM service URL
IamService:ServiceName - "promotion-service"
Jwt:Authority - JWT issuer authority URL
Jwt:Audience - "goodgo-api"
Jwt:RequireHttpsMetadata - false
Jwt:Secret - JWT signing key
Jwt:Issuer - "goodgo-platform"
Jwt:AccessTokenExpiryMinutes - 15
Jwt:RefreshTokenExpiryDays - 7
RabbitMQ:Host - RabbitMQ host
RabbitMQ:Port - 5672
RabbitMQ:Username/Password - guest/guest
Redis:ConnectionString - Redis connection string
Serilog - Structured logging config (Console sink)
9.2 Environment Variables
| Variable | Description |
|---|---|
| ASPNETCORE_ENVIRONMENT | Development / Production |
| ASPNETCORE_URLS | http://+:8080 (Docker) |
| DATABASE_URL | Fallback connection string |
9.3 Build Configuration
- Target Framework: net10.0
- Language Version: C# 14
- Nullable: enabled
- ImplicitUsings: enabled
- TreatWarningsAsErrors: true
- GenerateDocumentationFile: true
- Suppressed warnings: 1591 (missing XML doc), CA2017
10. MediatR Pipeline Behaviors
Registered in order:
- LoggingBehavior - logs request name, elapsed time (Stopwatch), and errors
- ValidatorBehavior - runs all FluentValidation validators for the request type, throws ValidationException on failure
- TransactionBehavior - wraps Commands in a database transaction (skips Queries based on name ending with "Query"), uses EF Core ExecutionStrategy for retry
11. Infrastructure Patterns
DbContext (PromotionServiceContext)
- Implements
IUnitOfWork - DbSets: Campaigns, Vouchers, Redemptions
SaveEntitiesAsync()dispatches domain events before saving- Transaction management:
BeginTransactionAsync(),CommitTransactionAsync(),RollbackTransaction()
Repositories
CampaignRepository- ICampaignRepository implementation, uses Include(Vouchers) for GetByIdAsyncRedemptionRepository- IRedemptionRepository implementation
Idempotency
IRequestManager/RequestManager- tracks client request IDs to prevent duplicate processingClientRequestentity (Id, Name, Time) - registered but table not included in migration
DI Registration (DependencyInjection.cs)
- PromotionServiceContext with Npgsql (retry on failure: 5 attempts, 30s max delay)
- ICampaignRepository -> CampaignRepository (Scoped)
- IRedemptionRepository -> RedemptionRepository (Scoped)
- IRequestManager -> RequestManager (Scoped)
12. Docker
- Dockerfile: Multi-stage build (sdk:10.0 -> aspnet:10.0)
- Runtime user: dotnetuser (UID 1001, GID 1001)
- Port: 8080
- Healthcheck: curl http://localhost:8080/health/live (30s interval, 3 retries)
- docker-compose.yml: Template-level (uses "myservice" naming, not production-ready), includes PostgreSQL 16 and Redis 7
13. Known Gaps and Issues
-
Missing Handlers:
ExchangeVoucherCommand,PurchaseVoucherCommand,SearchVouchersQuery,GetCampaignStatisticsQuery, andGetCampaignVouchersQueryare defined but have no handler implementations. -
VoucherRedeemedDomainEvent: Defined but never raised by any code.
-
No FluentValidation Validators: No validator classes exist in the codebase despite FluentValidation being registered in the pipeline. Commands have no request validation beyond domain-level checks.
-
No Integration Event Handlers: Domain events are dispatched but no handlers exist. No RabbitMQ integration despite RabbitMQ config being present in appsettings.
-
ClientRequest Table Missing from Migration: The
ClientRequestentity for idempotency is registered in DI but its table is not created in the migration. -
docker-compose.yml uses template naming: Service is named "myservice-api" instead of "promotion-service".
-
SampleDomainException: Leftover from template, unused.
-
Redemption table has no FK constraints: No foreign keys to vouchers or campaigns tables at the database level.
-
Redis health check package included but Redis is not used in the service code.