Files
pos-system/services/wallet-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

27 KiB

WalletService - Service Documentation

Auto-generated from source code audit. Last updated: 2026-03-13.

Overview

WalletService is a microservice responsible for digital wallet management, loyalty points, escrow/hold operations, and payment gateway integration. It follows Clean Architecture + CQRS patterns with MediatR.

  • Port: 5004 (Development)
  • Database: wallet_service (Neon PostgreSQL)
  • Framework: .NET 10.0, ASP.NET Core
  • Auth: JWT Bearer (Duende IdentityServer)
  • Payment Gateway: VNPay (sandbox, API v2.1.0)
  • Multi-tenancy: User-level tenant isolation via EF Core global query filters + PostgreSQL RLS session variables
  • Multi-currency: VND (id=1), USD (id=2), PPoint/Loyalty Points (id=3)

Key Capabilities

  1. Wallet Management - Create, deposit, withdraw, transfer, freeze/unfreeze/close wallets with multi-currency support
  2. Currency Exchange - Atomic in-wallet exchange between VND, USD, and PPoint with configurable rates
  3. Loyalty Points - Separate point account system with earn, spend, expire, bonus, and admin adjustment
  4. Escrow/Holds - Lock funds for campaigns/orders with partial execute/release/cancel
  5. Payment Gateway - VNPay integration for external payments with IPN callback validation
  6. Admin Backoffice - Full admin CRUD, statistics, search, freeze/unfreeze, balance/points adjustment

API Endpoints

Wallets (/api/v1/wallets) - [Authorize]

Method Path Description Auth
GET /{userId:guid} Get wallet by user ID Bearer
POST / Create new wallet Bearer
POST /{userId:guid}/deposit Deposit money into wallet Bearer
POST /{userId:guid}/withdraw Withdraw money from wallet Bearer
GET /{userId:guid}/transactions Get wallet transactions (paginated) Bearer

Points (/api/v1/points) - [Authorize]

Method Path Description Auth
GET /{userId:guid} Get point account by user ID Bearer
POST / Create new point account Bearer
POST /{userId:guid}/earn Earn points Bearer
POST /{userId:guid}/spend Spend points Bearer
GET /{userId:guid}/transactions Get point transactions (paginated) Bearer

Payments (/api/v1/payments)

Method Path Description Auth
POST /create Create payment via gateway (VNPay) Bearer
GET /{orderId:guid} Get payment by order ID Bearer
GET /vnpay/callback VNPay IPN callback Anonymous
GET /vnpay/return VNPay customer return URL Anonymous

Escrow/Holds (/api/v1/wallets/{walletId:guid}/holds) - [Authorize]

Method Path Description Auth
POST / Create escrow hold Bearer
GET /{holdId:guid} Get hold by ID (501 Not Implemented) Bearer
POST /{holdId:guid}/execute Execute (commit) hold portion Bearer
POST /{holdId:guid}/release Release hold portion back to wallet Bearer
POST /{holdId:guid}/cancel Cancel hold, release all remaining Bearer

Admin Wallets (/api/v1/admin/wallets) - [Authorize(Roles = "Admin,SuperAdmin")]

Method Path Description Auth
GET / Get all wallets (paginated, filterable) Admin
GET /{walletId:guid} Get wallet by ID with details Admin
POST /{walletId:guid}/freeze Freeze wallet Admin
POST /{walletId:guid}/unfreeze Unfreeze wallet Admin
POST /{walletId:guid}/adjust Adjust wallet balance (credit/debit) Admin
GET /statistics Get wallet statistics Admin
GET /search Search wallets by userId/walletId/status Admin

Admin Points (/api/v1/admin/points) - [Authorize(Roles = "Admin,SuperAdmin")]

Method Path Description Auth
GET / Get all point accounts (paginated, filterable) Admin
GET /{accountId:guid} Get point account by ID Admin
POST /{accountId:guid}/adjust Adjust points (add/subtract) Admin
POST /{accountId:guid}/bonus Grant bonus points Admin
GET /statistics Get points statistics Admin
GET /search Search point accounts by userId Admin

Health (/api/v1/health) - [AllowAnonymous]

Method Path Description
GET /live Liveness probe
GET /ready Readiness probe

Also mapped via MapHealthChecks: /health, /health/live, /health/ready


Commands

Wallet Commands

Command Input Logic Validator
CreateWalletCommand UserId, Currency="VND" Check duplicate by userId, parse CurrencyType, create Wallet + initial WalletItem(balance=0), raise WalletCreatedDomainEvent UserId NotEmpty; Currency NotEmpty, MaxLength(10)
DepositCommand UserId, Amount, Description, ReferenceId? Get wallet by userId, deposit with default CurrencyType, add WalletTransaction(Credit), raise WalletBalanceChangedDomainEvent UserId NotEmpty; Amount > 0, <= 1B; Description NotEmpty, MaxLength(500); ReferenceId MaxLength(100)
WithdrawCommand UserId, Amount, Description, ReferenceId? Get wallet by userId, validate sufficient balance, withdraw with default CurrencyType, add WalletTransaction(Debit), raise WalletBalanceChangedDomainEvent Same as Deposit
ExchangeCommand UserId, FromAmount, FromCurrencyTypeId, ToCurrencyTypeId, CustomRate? Get wallet, parse CurrencyTypes from IDs, calculate rate (custom or from BaseExchangeRate), atomic Withdraw + Deposit within aggregate, raise WalletExchangedDomainEvent UserId NotEmpty; FromAmount > 0; From/ToCurrencyTypeId > 0 and not equal; CustomRate > 0 (if provided)

Escrow Commands

Command Input Logic Validator
CreateHoldCommand UserId, Amount, CurrencyCode, ReferenceType, ReferenceId, Description, ExpiresAt? Get wallet by userId, parse CurrencyType, check balance, subtract from available, create HoldItem, add WalletTransaction(HoldCreated), raise EscrowHeldDomainEvent None
ExecuteHoldCommand WalletId, HoldId, Amount, ExecutionRef? Get wallet by ID, find hold, validate active+not expired, execute portion, add WalletTransaction(HoldExecuted), raise EscrowExecutedDomainEvent None
ReleaseHoldCommand WalletId, HoldId, Amount? (null=all) Get wallet by ID, find hold, release portion back to available balance, add WalletTransaction(HoldReleased), raise EscrowReleasedDomainEvent None
CancelHoldCommand WalletId, HoldId Get wallet by ID, release all remaining hold amount back to wallet None

Points Commands

Command Input Logic Validator
CreatePointAccountCommand UserId Check duplicate, create PointAccount with 0 points UserId NotEmpty
EarnPointsCommand UserId, Points, Source, Description, ExpiryMonths?=12 Get account by userId, earn points, add PointTransaction(Earn), raise PointsEarnedDomainEvent UserId NotEmpty; Points > 0; Source NotEmpty, MaxLength(100); Description NotEmpty, MaxLength(500); ExpiryMonths > 0, <= 120
SpendPointsCommand UserId, Points, Source, Description Get account by userId, validate sufficient points, spend, add PointTransaction(Spend), raise PointsSpentDomainEvent UserId NotEmpty; Points > 0; Source NotEmpty, MaxLength(100); Description NotEmpty, MaxLength(500)

Payment Commands

Command Input Logic Validator
CreatePaymentCommand OrderId, Amount, Currency, GatewayName, ReturnUrl, IpAddress Find gateway by name, create Payment entity, call gateway.CreatePaymentAsync, mark Processing or Failed, raise PaymentCreatedDomainEvent OrderId NotEmpty; Amount > 0; Currency NotEmpty, MaxLength(10); GatewayName NotEmpty, MaxLength(50); ReturnUrl valid URL; IpAddress NotEmpty
ProcessPaymentCallbackCommand GatewayName, Parameters (dict) Find gateway, validate HMAC-SHA512 hash, find payment by orderId (vnp_TxnRef), complete or fail based on vnp_ResponseCode, raise PaymentCompletedDomainEvent or PaymentFailedDomainEvent GatewayName NotEmpty; Parameters NotNull, Count > 0

Admin Commands

Command Input Logic Validator
AdminFreezeWalletCommand WalletId, Reason, AdminId Get wallet by ID, call Freeze() (status Active->Frozen) None
AdminUnfreezeWalletCommand WalletId, Reason, AdminId Get wallet by ID, call Unfreeze() (status Frozen->Active) None
AdminAdjustBalanceCommand WalletId, Amount, CurrencyTypeId, Reason, AdminId Get wallet, positive amount=Deposit, negative=Withdraw, with admin reason None
AdminAdjustPointsCommand AccountId, Points, Reason, AdminId Get point account by ID, call AdjustPoints (positive=add, negative=subtract) None
AdminGrantBonusCommand AccountId, Points, Reason, ExpiryMonths?, AdminId Get point account, call AddBonusPoints with expiry date None

Queries

Query Input Output Logic
GetWalletQuery UserId WalletDto? (Id, UserId, Balance, Currency, Status, CreatedAt, UpdatedAt) Get wallet by userId, return default currency balance
GetWalletTransactionsQuery UserId, Page=1, PageSize=20 WalletTransactionsDto (paginated list) Get wallet, then paginated transactions
GetPointAccountQuery UserId PointAccountDto? (Id, UserId, TotalPoints, AvailablePoints, dates) Get point account by userId
GetPointTransactionsQuery UserId, Page=1, PageSize=20 PointTransactionsDto (paginated list) Get account, then paginated transactions
GetPaymentByOrderIdQuery OrderId PaymentDto? Find payment by orderId
GetPaymentByTransactionIdQuery TransactionId (string) PaymentDto? Find payment by gateway transaction ID
GetAllWalletsQuery Page, PageSize, Status?, Currency? AdminWalletsListDto (paginated) Admin: filter by status, include balances
GetWalletByIdQuery WalletId AdminWalletDetailDto? Admin: include balances + transactions count
SearchWalletsQuery UserId?, WalletId?, Status? List<AdminWalletDetailDto> (max 50) Admin: flexible search
GetWalletStatisticsQuery (none) WalletStatisticsDto Admin: total/active/frozen/closed counts, balance by currency, today's transactions
GetAllPointAccountsQuery Page, PageSize, MinPoints?, MaxPoints? AdminPointAccountsListDto (paginated) Admin: filter by point range
GetPointAccountByIdQuery AccountId AdminPointAccountDetailDto? Admin: include transaction count
SearchPointAccountsQuery UserId? List<AdminPointAccountDetailDto> (max 50) Admin: search by userId
GetPointsStatisticsQuery (none) PointsStatisticsDto Admin: totals for issued/available/spent/expired points, today's activity

Domain Model

Aggregate: Wallet (Root)

Entity: Wallet (extends Entity, IAggregateRoot)

Property Type Description
Id Guid Wallet ID (generated)
UserId Guid User ID from IAM Service
DefaultCurrencyTypeId int Default currency (1=VND, 2=USD, 3=PPoint)
StatusId int Wallet status (1=Active, 2=Frozen, 3=Closed)
CreatedAt DateTime Creation timestamp
UpdatedAt DateTime Last update timestamp
Balances IReadOnlyCollection<WalletItem> Multi-currency balances (one per currency)
Transactions IReadOnlyCollection<WalletTransaction> Transaction history
Holds IReadOnlyCollection<HoldItem> Active escrow holds

Behavior Methods:

  • Deposit(amount, currencyType, description, referenceId?) - Add funds
  • Withdraw(amount, currencyType, description, referenceId?) - Remove funds (validates balance)
  • Exchange(fromAmount, fromCurrency, toCurrency, customRate?) - Atomic currency exchange
  • TransferOut(amount, toWalletId, description) - Send to another wallet
  • TransferIn(amount, fromWalletId, description) - Receive from another wallet
  • Hold(amount, currencyType, referenceType, referenceId, description, expiresAt?) - Create escrow hold
  • ExecuteHold(holdId, amount, executionRef?) - Commit hold portion
  • ReleaseHold(holdId, amount?) - Return hold back to wallet
  • CancelHold(holdId) - Release all remaining
  • Freeze() - Status -> Frozen (blocks transactions)
  • Unfreeze() - Status -> Active
  • Close() - Status -> Closed (requires zero balances)

Entity: WalletItem (child of Wallet)

Property Type Description
Id Guid Item ID
WalletId Guid Parent wallet FK
CurrencyTypeId int Currency type
Balance decimal Current balance
CreatedAt / UpdatedAt DateTime Timestamps

Entity: WalletTransaction

Property Type Description
Id Guid Transaction ID
WalletId Guid Parent wallet FK
Amount Money (value object) Amount + Currency
TypeId int TransactionType enum ID
ReferenceId string? External reference
Description string Transaction description
BalanceAfter decimal Balance after this transaction
CreatedAt DateTime Timestamp

Entity: HoldItem (child of Wallet)

Property Type Description
Id Guid Hold ID
WalletId Guid Parent wallet FK
OriginalAmount decimal Initial held amount
RemainingAmount decimal Currently held
ExecutedAmount decimal Total committed
ReleasedAmount decimal Total returned
CurrencyTypeId int Currency type
ReferenceType string e.g., "CAMPAIGN", "ORDER"
ReferenceId Guid External reference ID
Description string Hold description
StatusId int HoldStatus enum ID
ExpiresAt DateTime? Optional expiration
CreatedAt / UpdatedAt DateTime Timestamps

Value Object: Money

  • Amount (decimal) + Currency (string, uppercase)
  • Methods: Add, Subtract, IsGreaterThanOrEqual

Enumerations:

Enumeration Values
CurrencyType VND(1, rate=1), USD(2, rate=25000), PPoint(3, rate=1000)
TransactionType Credit(1), Debit(2), TransferOut(3), TransferIn(4), Refund(5), HoldCreated(6), HoldExecuted(7), HoldReleased(8)
WalletStatus Active(1), Frozen(2), Closed(3)
HoldStatus Active(1), PartiallyReleased(2), Released(3), Executed(4), Cancelled(5)

Aggregate: PointAccount (Root)

Entity: PointAccount (extends Entity, IAggregateRoot)

Property Type Description
Id Guid Account ID
UserId Guid User ID from IAM Service
TotalPoints long Lifetime earned points
AvailablePoints long Currently usable points
CreatedAt / UpdatedAt DateTime Timestamps
Transactions IReadOnlyCollection<PointTransaction> Transaction history

Behavior Methods:

  • EarnPoints(points, source, description, expiresAt?) - Add points
  • SpendPoints(points, source, description) - Deduct points (validates balance)
  • ExpirePoints(points, description) - System expiration
  • AddBonusPoints(points, source, description, expiresAt?) - Special promotion bonus
  • AdjustPoints(adjustment, reason) - Admin manual adjustment

Entity: PointTransaction

Property Type Description
Id Guid Transaction ID
AccountId Guid Parent account FK
Points long Point amount
TypeId int PointTransactionType enum ID
Source string e.g., order ID, "ADMIN"
Description string Transaction description
ExpiresAt DateTime? Point expiration date
BalanceAfter long Balance after transaction
CreatedAt DateTime Timestamp

Enumeration: PointTransactionType

  • Earn(1), Spend(2), Expire(3), Adjust(4), Bonus(5)

Aggregate: Payment (Root)

Entity: Payment (extends Entity, IAggregateRoot) - uses private fields

Property Type Description
Id Guid Payment ID
OrderId Guid Associated order
Amount decimal Payment amount
Currency string Currency code
GatewayName string e.g., "VNPAY"
TransactionId string? Gateway transaction ID
PaymentUrl string? Customer redirect URL
StatusId int PaymentStatus enum ID
ErrorCode / ErrorMessage string? Failure details
CreatedAt DateTime Creation timestamp
CompletedAt DateTime? Completion timestamp

Behavior Methods:

  • MarkAsProcessing(paymentUrl, transactionId?) - Pending -> Processing
  • Complete(transactionId) - Pending/Processing -> Completed
  • Fail(errorCode, errorMessage) - Pending/Processing -> Failed
  • Refund() - Completed -> Refunded

Enumeration: PaymentStatus

  • Pending(1), Processing(2), Completed(3), Failed(4), Refunded(5)

Domain Events

Event Properties Raised When
WalletCreatedDomainEvent WalletId, UserId, OccurredAt Wallet constructor
WalletBalanceChangedDomainEvent WalletId, UserId, TransactionType, Amount, NewBalance Deposit, Withdraw, Transfer
WalletExchangedDomainEvent WalletId, UserId, FromCurrencyId, ToCurrencyId, FromAmount, ToAmount, Rate Currency exchange
EscrowHeldDomainEvent WalletId, HoldId, UserId, ReferenceType, ReferenceId, Amount, CurrencyTypeId Hold created
EscrowExecutedDomainEvent WalletId, HoldId, UserId, Amount, RemainingAmount, ExecutionRef Hold portion executed
EscrowReleasedDomainEvent WalletId, HoldId, UserId, Amount, RemainingAmount Hold portion released
PointsEarnedDomainEvent AccountId, UserId, Points, NewBalance, Source Points earned/bonus added
PointsSpentDomainEvent AccountId, UserId, Points, NewBalance, Source Points spent
PaymentCreatedDomainEvent PaymentId, OrderId, Amount, Currency, GatewayName Payment created
PaymentCompletedDomainEvent PaymentId, OrderId, TransactionId, Amount, Currency Payment completed
PaymentFailedDomainEvent PaymentId, OrderId, ErrorCode, ErrorMessage Payment failed

Domain Exceptions

Exception Base Description
WalletDomainException Exception Base wallet domain error
InsufficientBalanceException WalletDomainException Balance < requested amount
PointsDomainException Exception Base points domain error
InsufficientPointsException PointsDomainException Points < requested amount

Database Schema

Table: wallets

Column Type Nullable Description
id uuid NO PK, ValueGeneratedNever
user_id uuid NO User ID from IAM
default_currency_type_id int NO Default currency (default=1/VND)
status_id int NO WalletStatus enum ID
created_at timestamp NO Creation time
updated_at timestamp NO Last update

Indexes: ix_wallets_user_id (unique) Query Filter: Tenant isolation by user_id (bypassed for admin/service calls)

Table: wallet_items

Column Type Nullable Description
id uuid NO PK
wallet_id uuid NO FK -> wallets
currency_type_id int NO CurrencyType enum ID
balance decimal(18,2) NO Current balance
created_at timestamp NO
updated_at timestamp NO

Indexes: ix_wallet_items_wallet_currency (unique: wallet_id + currency_type_id), ix_wallet_items_wallet_id Cascade: Delete with parent wallet

Table: wallet_transactions

Column Type Nullable Description
id uuid NO PK
wallet_id uuid NO FK -> wallets
amount decimal(18,2) NO Transaction amount (owned: Money.Amount)
currency varchar(3) NO Currency code (owned: Money.Currency)
type_id int NO TransactionType enum ID
reference_id varchar(100) YES External reference
description varchar(500) NO Description
balance_after decimal(18,2) NO Balance after transaction
created_at timestamp NO

Indexes: ix_wallet_transactions_wallet_id, ix_wallet_transactions_created_at Cascade: Delete with parent wallet

Table: wallet_holds

Column Type Nullable Description
id uuid NO PK
wallet_id uuid NO FK -> wallets
original_amount decimal(18,2) NO Initially held
remaining_amount decimal(18,2) NO Currently held
executed_amount decimal(18,2) NO Total committed
released_amount decimal(18,2) NO Total returned
currency_type_id int NO FK (restrict delete)
reference_type varchar(50) NO e.g., "CAMPAIGN"
reference_id uuid NO External ref ID
description varchar(500) NO
status_id int NO FK (restrict delete)
created_at timestamp NO
updated_at timestamp NO
expires_at timestamp YES Optional expiry

Indexes: ix_wallet_holds_wallet_id, ix_wallet_holds_reference (reference_type + reference_id), ix_wallet_holds_status_id Cascade: Delete with parent wallet

Table: point_accounts

Column Type Nullable Description
id uuid NO PK
user_id uuid NO User ID from IAM
total_points bigint NO Lifetime earned
available_points bigint NO Currently usable
created_at timestamp NO
updated_at timestamp NO

Indexes: ix_point_accounts_user_id (unique)

Table: point_transactions

Column Type Nullable Description
id uuid NO PK
account_id uuid NO FK -> point_accounts
points bigint NO Point amount
type_id int NO PointTransactionType enum ID
source varchar(100) NO Source identifier
description varchar(500) NO
expires_at timestamp YES Point expiration
balance_after bigint NO Balance after transaction
created_at timestamp NO

Indexes: ix_point_transactions_account_id, ix_point_transactions_created_at, ix_point_transactions_expires_at Cascade: Delete with parent account

Table: payments

Column Type Nullable Description
id uuid NO PK
order_id uuid NO Associated order
amount decimal(18,2) NO Payment amount
currency varchar(10) NO Currency code
gateway_name varchar(50) NO e.g., "VNPAY"
transaction_id varchar(255) YES Gateway transaction ID
payment_url varchar(2048) YES Customer redirect URL
status_id int NO PaymentStatus enum ID
error_code varchar(50) YES
error_message varchar(500) YES
created_at timestamp NO
completed_at timestamp YES

Indexes: ix_payments_order_id, ix_payments_transaction_id, ix_payments_status_id, ix_payments_gateway_name

Migrations

  1. 20260117141548_AddWalletHolds - Initial schema with holds
  2. 20260306175521_PhaseTwo - Phase 2 additions (payments, multi-currency, etc.)

Dependencies

NuGet Packages

API Layer:

  • MediatR 12.4.1 (CQRS)
  • FluentValidation 11.11.0 + DI Extensions
  • Microsoft.AspNetCore.Authentication.JwtBearer 10.0.1
  • Asp.Versioning.Mvc 8.1.0
  • AspNetCore.HealthChecks.NpgSql 8.0.2, Redis 8.0.1
  • Hellang.Middleware.ProblemDetails 6.5.1
  • Serilog.AspNetCore 8.0.3, Console 6.0.0, Seq 8.0.0
  • Swashbuckle.AspNetCore 7.2.0 + Annotations
  • EF Core Design 10.0.0

Domain Layer:

  • MediatR.Contracts 2.0.1 (domain events only)

Infrastructure Layer:

  • Microsoft.EntityFrameworkCore 10.0.0
  • Npgsql.EntityFrameworkCore.PostgreSQL 10.0.0
  • MediatR 12.4.1 (domain event dispatch)
  • Dapper 2.1.35 (read-optimized queries)
  • Polly 8.5.0, Microsoft.Extensions.Http.Polly 9.0.0
  • StackExchange.Redis 2.8.16

Service Dependencies

  • IAM Service: JWT token validation, user ID claims
  • Order Service: OrderId referenced in Payments

Configuration

appsettings.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Host=..neon.tech;Database=wallet_service;..."
  },
  "Redis": {
    "ConnectionString": "localhost:6379"
  },
  "Jwt": {
    "Secret": "...",
    "Issuer": "goodgo-platform",
    "Audience": "goodgo-services",
    "AccessTokenExpiryMinutes": 15,
    "RefreshTokenExpiryDays": 7
  }
}

appsettings.Development.json

{
  "VnPay": {
    "TmnCode": "GOODGO01",
    "HashSecret": "GOODGOSECRETKEYFORSANDBOX2026",
    "PaymentUrl": "https://sandbox.vnpayment.vn/paymentv2/vpcpay.html",
    "ReturnUrl": "http://localhost:3001/payment/return",
    "ApiUrl": "https://sandbox.vnpayment.vn/merchant_webapi/api/transaction"
  }
}

Middleware Pipeline Order

  1. Serilog request logging
  2. ProblemDetails (RFC 7807)
  3. Swagger (Development only)
  4. CORS (AllowAny)
  5. Routing
  6. Authentication (JWT Bearer)
  7. Authorization
  8. TenantMiddleware (RLS - sets PostgreSQL session variables)
  9. Health checks + Controllers

MediatR Pipeline Behaviors

  1. LoggingBehavior<,> - Request/response logging
  2. ValidatorBehavior<,> - FluentValidation in pipeline
  3. TransactionBehavior<,> - Auto transaction wrapping for commands

Multi-Tenancy

  • Wallet isolation: User-level via EF Core global query filter on Wallet.UserId
  • Bypass: Admin users and service-to-service calls (via X-Service-Call: internal header)
  • PostgreSQL RLS: TenantMiddleware sets SET LOCAL app.current_shop_id and app.current_merchant_id session variables
  • Adapter pattern: ITenantProvider (API layer) -> WalletTenantProviderAdapter -> IWalletTenantProvider (Infrastructure layer)

Tests

Unit Tests (tests/WalletService.UnitTests/)

  • Domain/WalletTests.cs - Wallet entity behavior tests
  • Domain/PointAccountTests.cs - PointAccount entity behavior tests
  • Domain/HoldItemTests.cs - HoldItem entity behavior tests
  • Application/Commands/CreatePaymentCommandHandlerTests.cs - Payment creation handler tests
  • Application/Commands/ProcessPaymentCallbackCommandHandlerTests.cs - Callback processing tests
  • Application/EscrowCommandHandlersTests.cs - Escrow command handler tests

Functional Tests (tests/WalletService.FunctionalTests/)

  • CustomWebApplicationFactory.cs - Test server setup
  • Controllers/HealthControllerTests.cs - Health endpoint tests

Exchange Rates (Hardcoded in CurrencyType)

From To Rate Example
VND USD 0.00004 25,000 VND = 1 USD
VND PPoint 0.001 1,000 VND = 1 PPoint
USD VND 25,000 1 USD = 25,000 VND
USD PPoint 25 1 USD = 25 PPoint
PPoint VND 1,000 1 PPoint = 1,000 VND
PPoint USD 0.04 25 PPoint = 1 USD

Formula: rate = fromCurrency.BaseExchangeRate / toCurrency.BaseExchangeRate