Files
Ho Ngoc Hai 76d75c753b Migrate
2026-05-23 18:37:02 +07:00

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:DefaultConnection or DATABASE_URL env 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. Raises CampaignActivatedDomainEvent.

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(). Raises CampaignCancelledDomainEvent.

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. Raises VoucherClaimedDomainEvent.

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 -> Paused
    • Cancel() - any except Completed/Cancelled -> Cancelled
    • Complete() - Active/Paused -> Completed
    • Update(name?, description?, startDate?, endDate?, maxPerUser?) - update fields
    • GenerateVouchers(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 code
    • GetUserVouchers(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 expiry
    • Redeem(amount) - deducts from RemainingValue, returns actual amount deducted, status becomes PartiallyRedeemed or FullyRedeemed
    • Expire() - marks as Expired
    • ExtendExpiry(additionalDays) - extends ExpiresAt
    • IsValidForRedemption() - checks status is Claimed/PartiallyRedeemed, has remaining value, not expired
    • IsExpired (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 violations
  • CampaignNotActiveException - operating on non-active campaign
  • VoucherAlreadyClaimedException - claiming an already-claimed voucher
  • VoucherExpiredException - using an expired voucher
  • InsufficientVoucherValueException - voucher has insufficient remaining value
  • DomainException - 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_id on (merchant_id)
  • ix_campaigns_status_id on (status_id)
  • ix_campaigns_date_range on (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_code on (code) UNIQUE
  • ix_vouchers_owner_id on (owner_id)
  • ix_vouchers_campaign_id on (campaign_id)
  • ix_vouchers_status_id on (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_id on (voucher_id)
  • ix_redemptions_user_id on (user_id)
  • ix_redemptions_campaign_id on (campaign_id)
  • ix_redemptions_order_id on (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}/holds
  • ExecuteHoldAsync - POST /api/v1/wallets/{walletId}/holds/{holdId}/execute
  • ReleaseHoldAsync - POST /api/v1/wallets/{walletId}/holds/{holdId}/release
  • CancelHoldAsync - POST /api/v1/wallets/{walletId}/holds/{holdId}/cancel
  • GetWalletByUserIdAsync - 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:

  1. LoggingBehavior - logs request name, elapsed time (Stopwatch), and errors
  2. ValidatorBehavior - runs all FluentValidation validators for the request type, throws ValidationException on failure
  3. 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 GetByIdAsync
  • RedemptionRepository - IRedemptionRepository implementation

Idempotency

  • IRequestManager / RequestManager - tracks client request IDs to prevent duplicate processing
  • ClientRequest entity (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

  1. Missing Handlers: ExchangeVoucherCommand, PurchaseVoucherCommand, SearchVouchersQuery, GetCampaignStatisticsQuery, and GetCampaignVouchersQuery are defined but have no handler implementations.

  2. VoucherRedeemedDomainEvent: Defined but never raised by any code.

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

  4. No Integration Event Handlers: Domain events are dispatched but no handlers exist. No RabbitMQ integration despite RabbitMQ config being present in appsettings.

  5. ClientRequest Table Missing from Migration: The ClientRequest entity for idempotency is registered in DI but its table is not created in the migration.

  6. docker-compose.yml uses template naming: Service is named "myservice-api" instead of "promotion-service".

  7. SampleDomainException: Leftover from template, unused.

  8. Redemption table has no FK constraints: No foreign keys to vouchers or campaigns tables at the database level.

  9. Redis health check package included but Redis is not used in the service code.