29 KiB
Order Service - Service Documentation
Auto-generated from source code audit. Last updated: 2026-03-13.
Overview
Order Service is the central order processing microservice for the GoodGo POS platform. It orchestrates order creation, payment, fulfillment, returns, exchanges, and reporting across multiple business verticals (Restaurant/F&B, Retail, Spa/Services).
- Framework: .NET 10.0, C# 14
- Architecture: Clean Architecture + CQRS (MediatR 12.4.1)
- Database: PostgreSQL (Neon) via EF Core 10 (writes) + Dapper 2.1 (reads)
- Real-time: SignalR with Redis backplane + MessagePack protocol
- Port: 5017 (Development)
- Base Route:
api/v1/orders,api/v1/admin/orders,api/v1/reports - SignalR Hub:
/hubs/pos - Health Checks:
/health,/health/live,/health/ready
Key Features
- Multi-vertical order processing via Strategy pattern (Physical, PreparedFood, Service)
- Multi-payment support (cash, card, VNPay, Momo, QR, bank transfer)
- Real-time POS/KDS notifications via SignalR
- Multi-tenant row-level security (EF Core global query filters + PostgreSQL RLS)
- Return and exchange workflows
- Revenue analytics, staff performance, and End-of-Day reports
- Idempotency support for duplicate request detection
API Endpoints
OrdersController (api/v1/orders)
| Method | Route | Description | Request | Response |
|---|---|---|---|---|
| POST | /api/v1/orders |
Create a new order | CreateOrderCommand (body) |
201 Created - CreateOrderResult |
| GET | /api/v1/orders/{id} |
Get order by ID | id (path), shopId (query) |
200 OK - OrderDto / 404 |
| GET | /api/v1/orders |
List orders by shop (paginated) | shopId, status?, fromDate?, toDate?, page, pageSize (query) |
200 OK - PagedResult<OrderSummaryDto> |
| POST | /api/v1/orders/{id}/pay |
Process payment | id (path), shopId (query), PayOrderRequest (body) |
200 OK - PayOrderResult / 400 |
| POST | /api/v1/orders/{id}/payment-callback |
Payment gateway callback | id (path), PaymentCallbackRequest (body) |
200 OK - CompleteOrderPaymentResult / 400 |
| POST | /api/v1/orders/{id}/cancel |
Cancel an order | id (path), shopId (query), CancelOrderRequest (body) |
200 OK - CancelOrderResult |
| POST | /api/v1/orders/{id}/complete |
Complete an order | id (path), shopId (query) |
200 OK - CompleteOrderResult |
| GET | /api/v1/orders/dashboard |
POS dashboard stats | shopId, period? ("today"/"7d"/"30d") (query) |
200 OK - PosDashboardDto |
| GET | /api/v1/orders/active-by-table |
Active orders grouped by table | shopId (query) |
200 OK - List<ActiveTableOrderDto> |
| POST | /api/v1/orders/returns |
Create a return | CreateReturnCommand (body) |
201 Created - CreateReturnResult / 400 |
| POST | /api/v1/orders/exchanges |
Create an exchange | CreateExchangeCommand (body) |
201 Created - CreateExchangeResult / 400 |
| GET | /api/v1/orders/{orderId}/returns |
Get return history | orderId (path) |
200 OK - List<ReturnOrderDto> |
| GET | /api/v1/orders/customer/{customerId} |
Get orders by customer | customerId (path), page, pageSize (query) |
200 OK - PagedResult<OrderSummaryDto> |
AdminOrdersController (api/v1/admin/orders)
| Method | Route | Description | Request | Response |
|---|---|---|---|---|
| GET | /api/v1/admin/orders |
List all orders (admin) | shopId?, customerId?, status?, fromDate?, toDate?, minAmount?, maxAmount?, page, pageSize (query) |
200 OK - PagedResult<OrderSummaryDto> |
| GET | /api/v1/admin/orders/stats |
Order statistics | shopId?, fromDate?, toDate? (query) |
200 OK - OrderStatsDto |
| GET | /api/v1/admin/orders/export |
Export orders as CSV | shopId?, fromDate?, toDate? (query) |
200 OK - CSV file download |
ReportsController (api/v1/reports)
| Method | Route | Description | Request | Response |
|---|---|---|---|---|
| GET | /api/v1/reports/revenue |
Revenue report (daily/weekly/monthly) | shopId, period, fromDate?, toDate? (query) |
200 OK - RevenueReportDto |
| GET | /api/v1/reports/top-products |
Top selling products | shopId, limit?, fromDate?, toDate? (query) |
200 OK - List<TopProductDto> |
| GET | /api/v1/reports/revenue-analytics |
Advanced revenue analytics | shopId, startDate, endDate, period? (query) |
200 OK - RevenueAnalyticsDto |
| GET | /api/v1/reports/staff-performance |
Staff performance metrics | shopId, startDate, endDate (query) |
200 OK - StaffPerformanceDto |
| GET | /api/v1/reports/eod |
End-of-Day report | shopId, date? (query) |
200 OK - EodReportDto |
| POST | /api/v1/reports/close-day |
Close business day | CloseDayRequest (body) |
200 OK - CloseDayResult / 400 |
SignalR Hub (/hubs/pos)
| Method | Direction | Description |
|---|---|---|
JoinShop(shopId) |
Client -> Server | Join shop group for all POS updates |
JoinKds(shopId) |
Client -> Server | Join KDS group for kitchen updates |
JoinPos(shopId) |
Client -> Server | Join POS terminal group |
LeaveShop(shopId) |
Client -> Server | Leave shop group |
LeaveKds(shopId) |
Client -> Server | Leave KDS group |
LeavePos(shopId) |
Client -> Server | Leave POS terminal group |
OrderCreated |
Server -> Client | New order created notification |
OrderUpdated |
Server -> Client | Order updated notification |
OrderStatusChanged |
Server -> Client | Order status change notification |
KitchenTicketCreated |
Server -> Client | New kitchen ticket (to KDS) |
KitchenTicketUpdated |
Server -> Client | Kitchen ticket update (to KDS) |
PaymentCompleted |
Server -> Client | Payment completed notification |
TableStatusChanged |
Server -> Client | Table status change notification |
Groups: shop:{shopId}, kds:{shopId}, pos:{shopId}
Auth: JWT required (token via query string access_token for WebSocket)
Shop Access: Validated via shop_id JWT claim (prevents cross-tenant access)
Commands
CreateOrderCommand
- Input:
ShopId(Guid),CustomerId?(Guid?),Items(List<OrderItemRequest>),DiscountAmount?,DiscountType?,DiscountReference?,TableId? - OrderItemRequest:
ProductId,ProductName,ProductType("Physical"|"Service"|"PreparedFood"),Quantity,UnitPrice,TrackInventory - Logic: Creates Order aggregate -> adds items -> validates each item via strategy (RetailStrategy/ServiceStrategy/FnbStrategy) -> executes each item via strategy (inventory deduction / kitchen ticket / booking) -> applies discount -> marks as Validated -> saves -> sends SignalR notification
- Result:
CreateOrderResult(OrderId, TotalAmount, Status) - Validator: ShopId required, Items non-empty, each item validated (ProductId, ProductName, ProductType in ["Physical","Service","PreparedFood"], Quantity > 0, UnitPrice >= 0)
PayOrderCommand
- Input:
OrderId,ShopId,PaymentMethod("cash"|"card"|"vnpay"|"momo"|"qr"|"transfer"),AmountTendered?,ReturnUrl?,IpAddress? - Logic: Loads order -> verifies shop ownership -> routes to payment flow:
- Cash: Validates sufficient amount -> generates CASH-* transaction ID -> calculates change -> marks Paid+Processing
- Card/QR/Transfer: Generates CARD-* transaction ID -> marks Paid+Processing (POS terminal confirmed)
- VNPay/Momo: Calls wallet-service CreatePaymentAsync -> marks PaymentPending -> returns payment URL for redirect
- Result:
PayOrderResult(Success, Status, PaymentUrl?, ChangeAmount?, TransactionId?, ErrorMessage?) - Validator: OrderId/ShopId required, PaymentMethod in supported list, AmountTendered required for cash, ReturnUrl required for vnpay/momo
CompleteOrderPaymentCommand
- Input:
OrderId,GatewayTransactionId,IsSuccess,GatewayResponseCode? - Logic: Called by wallet-service callback after online payment. If success -> CompletePayment + MarkAsProcessing. If failure -> Cancel order.
- Result:
CompleteOrderPaymentResult(Success, Status, ErrorMessage?) - Validator: OrderId required, GatewayTransactionId required (max 200), GatewayResponseCode max 50
CancelOrderCommand
- Input:
OrderId,ShopId,Reason - Logic: Loads order -> verifies shop ownership -> calls Order.Cancel(reason) (domain validates not Completed/already Cancelled) -> saves -> sends SignalR notification
- Result:
CancelOrderResult(Success, Status) - Validator: OrderId/ShopId required, Reason required (max 500)
CompleteOrderCommand
- Input:
OrderId,ShopId - Logic: Loads order -> verifies shop ownership -> calls Order.MarkAsCompleted() (domain validates must be Processing) -> saves -> sends SignalR notification
- Result:
CompleteOrderResult(Success, Status) - Validator: OrderId/ShopId required
CreateReturnCommand
- Input:
ShopId,OriginalOrderId,Items(List<ReturnItemDto>),Reason - ReturnItemDto:
OrderItemId,Quantity,Reason? - Logic: Validates original order (must be Completed/Paid, same shop) -> creates new return order with items from original -> marks Validated -> ProcessReturn (sets isReturn, returnReason, originalOrderId) -> saves
- Result:
CreateReturnResult(Success, ReturnOrderId?, RefundAmount?, ErrorMessage?) - Validator: ShopId/OriginalOrderId required, Reason required (max 1000), Items non-empty, each item: OrderItemId required, Quantity > 0
CreateExchangeCommand
- Input:
ShopId,OriginalOrderId,ReturnItems(List<ReturnItemDto>),NewItems(List<ExchangeItemDto>),Reason - ExchangeItemDto:
ProductId,Quantity,UnitPrice - Logic: Validates original order -> creates return order (step 1) -> creates new order with replacement items (step 2) -> calculates price difference -> saves both in same transaction -> raises OrderExchangedDomainEvent
- Result:
CreateExchangeResult(Success, ReturnOrderId?, NewOrderId?, PriceDifference?, ErrorMessage?) - Validator: ShopId/OriginalOrderId required, Reason required, ReturnItems/NewItems non-empty with item-level validation
CloseDayCommand
- Input:
ShopId,CloseDate - Logic: Checks for pending/in-progress orders (status 1,2,3,4,7) via Dapper -> generates EOD report via GetEodReportQuery -> returns report with warning if pending orders exist
- Result:
CloseDayResult(Success, Report?, Message?, PendingOrderCount) - Validator: ShopId required, CloseDate required and not in future
Queries
GetOrderByIdQuery
- Input:
OrderId,ShopId - Logic: Dapper query joining
orders+order_statuses+order_items, filtered by OrderId and ShopId - Result:
OrderDto(with items) or null
ListOrdersByShopQuery
- Input:
ShopId,Status?,FromDate?,ToDate?,Page,PageSize - Logic: Dapper query with dynamic WHERE clause, paginated (LIMIT/OFFSET), ordered by created_at DESC
- Result:
PagedResult<OrderSummaryDto>
AdminListOrdersQuery
- Input:
ShopId?,CustomerId?,Status?,FromDate?,ToDate?,MinAmount?,MaxAmount?,Page,PageSize - Logic: Dapper query with all optional filters, no shop restriction (admin access)
- Result:
PagedResult<OrderSummaryDto>
GetOrdersByCustomerQuery
- Input:
CustomerId,Page,PageSize - Logic: Dapper query filtered by customer_id, paginated
- Result:
PagedResult<OrderSummaryDto>
GetActiveTableOrdersQuery
- Input:
ShopId - Logic: Gets orders with status_id=2 (Validated) that have table_id, then batch-loads items via ANY(@OrderIds)
- Result:
List<ActiveTableOrderDto>(each with items)
GetOrderReturnsQuery
- Input:
OrderId - Logic: Uses EF Core repository
GetReturnsByOriginalOrderIdAsyncto find return orders - Result:
List<ReturnOrderDto>
GetOrderStatsQuery
- Input:
ShopId?,FromDate?,ToDate? - Logic: Dapper aggregate (COUNT, SUM, AVG) + GROUP BY status name
- Result:
OrderStatsDto(TotalOrders, TotalRevenue, AverageOrderValue, OrdersByStatus)
ExportOrdersQuery
- Input:
ShopId?,FromDate?,ToDate? - Logic: Dapper query all matching orders -> generates CSV string
- Result:
ExportOrdersResult(FileName, CsvContent)(served as file download)
GetPosDashboardQuery
- Input:
ShopId,Period?("today"|"7d"|"30d") - Logic: Multiple Dapper queries: aggregate stats, top 5 products, payment breakdown by status, hourly revenue, last 10 orders
- Result:
PosDashboardDto(Revenue, OrderCount, ItemsSold, AvgOrderValue, PopularItems, PaymentBreakdown, HourlyRevenue, RecentOrders)
GetRevenueReportQuery
- Input:
Period("daily"|"weekly"|"monthly"),ShopId,FromDate?,ToDate? - Logic: Dapper with DATE_TRUNC grouping
- Result:
RevenueReportDto(Period, ShopId, TotalRevenue, TotalOrders, Data[])
GetTopProductsQuery
- Input:
ShopId,Limit,FromDate?,ToDate? - Logic: Dapper GROUP BY product_id, product_name, ORDER BY quantity DESC
- Result:
List<TopProductDto>
GetRevenueAnalyticsQuery
- Input:
ShopId,StartDate,EndDate,Period - Logic: 6 Dapper queries: current aggregate, previous period revenue, trends, payment method breakdown, top 10 products, vertical breakdown. Calculates growth percentage.
- Result:
RevenueAnalyticsDto(TotalRevenue, PreviousPeriodRevenue, GrowthPercentage, TotalOrders, AverageOrderValue, Trends, PaymentMethods, TopProducts, VerticalBreakdown) - Validator: ShopId required, StartDate < EndDate, EndDate not future, Period in ["daily","weekly","monthly"]
GetStaffPerformanceQuery
- Input:
ShopId,StartDate,EndDate - Logic: Dapper GROUP BY staff_id, staff_name with FILTER for completed/cancelled, calculates completion rate and avg handling time
- Result:
StaffPerformanceDto(Staff[], ShopAverage) - Validator: ShopId required, StartDate < EndDate, EndDate not future
GetEodReportQuery
- Input:
ShopId,ReportDate - Logic: 4 Dapper queries: aggregate (total/completed/cancelled/revenue by payment type/discounts), payment breakdown, top 10 items, hourly revenue
- Result:
EodReportDto(ReportDate, ShopId, TotalOrders, CompletedOrders, CancelledOrders, TotalRevenue, CashRevenue, CardRevenue, OnlineRevenue, DiscountTotal, PaymentBreakdown, TopItems, HourlyRevenue) - Validator: ShopId required, ReportDate required and not future
Domain Model
Order (Aggregate Root)
Table: orders
Private Fields / Properties:
| Field | Type | Description |
|---|---|---|
_shopId / ShopId |
Guid | Shop that owns the order |
_customerId / CustomerId |
Guid? | Customer (null for walk-in) |
_tableId / TableId |
Guid? | Table for dine-in orders |
StatusId |
int | FK to order_statuses |
_status / Status |
OrderStatus | Resolved from StatusId |
_totalAmount / TotalAmount |
decimal | Calculated: sum(items) - discount |
_discountAmount / DiscountAmount |
decimal | Discount applied |
_discountType / DiscountType |
string? | Discount type (e.g. "percentage", "fixed") |
_discountReference / DiscountReference |
string? | Promotion/coupon reference |
_paymentMethod / PaymentMethod |
string? | "cash", "card", "vnpay", "momo", "qr", "transfer" |
_transactionId / TransactionId |
string? | External transaction ID |
_amountTendered / AmountTendered |
decimal? | Amount customer paid (cash) |
_changeAmount / ChangeAmount |
decimal? | Change returned (cash) |
_isReturn / IsReturn |
bool | Whether this is a return order |
_returnReason / ReturnReason |
string? | Reason for return |
_returnedAt / ReturnedAt |
DateTime? | When return was processed |
_originalOrderId / OriginalOrderId |
Guid? | Original order (for returns/exchanges) |
_createdAt / CreatedAt |
DateTime | Creation timestamp (UTC) |
_updatedAt / UpdatedAt |
DateTime? | Last update timestamp (UTC) |
_items / Items |
List<OrderItem> | Line items (owned collection) |
Behavior Methods:
| Method | Transitions | Domain Event | Validation |
|---|---|---|---|
Order(shopId, customerId?, tableId?) |
-> Draft | OrderCreatedDomainEvent |
ShopId != empty |
AddItem(item) |
(Draft only) | - | Status must be Draft |
MarkAsValidated() |
Draft -> Validated | - | Must be Draft, must have items |
MarkAsPaid(method, txnId, amountTendered?) |
Validated/PaymentPending -> Paid | OrderPaidDomainEvent |
Must be Validated or PaymentPending |
MarkAsPaymentPending(method, txnId) |
Validated -> PaymentPending | OrderPaymentPendingDomainEvent |
Must be Validated |
CompletePayment(gatewayTxnId) |
PaymentPending -> Paid | OrderPaidDomainEvent |
Must be PaymentPending |
MarkAsProcessing() |
Paid -> Processing | - | Must be Paid |
MarkAsCompleted() |
Processing -> Completed | OrderCompletedDomainEvent |
Must be Processing |
Cancel(reason) |
Any (except Completed/Cancelled) -> Cancelled | OrderCancelledDomainEvent |
Not Completed, not already Cancelled |
ProcessReturn(reason, originalOrderId?) |
-> Returned | OrderReturnedDomainEvent |
Reason required |
ApplyDiscount(amount, type?, reference?) |
- | - | Amount >= 0 |
OrderItem (Entity, Owned by Order)
Table: order_items
| Field | Type | Description |
|---|---|---|
Id |
Guid | Primary key |
_productId / ProductId |
Guid | Product from Catalog Service |
_productName / ProductName |
string | Snapshot of product name |
_productType / ProductType |
string | "Physical", "Service", "PreparedFood" |
_quantity / Quantity |
int | Quantity ordered |
_unitPrice / UnitPrice |
decimal | Unit price at order time |
TotalPrice |
decimal | Computed: Quantity * UnitPrice |
_status / Status |
string | "Pending", "Completed", "Failed" |
_trackInventory / TrackInventory |
bool | Auto-deduct inventory flag (default true) |
_metadata / Metadata |
string? | JSON metadata (e.g. appointment details) |
Methods: MarkAsCompleted(), MarkAsFailed()
OrderStatus (Enumeration)
| Id | Name | Description |
|---|---|---|
| 1 | Draft | Created but not validated |
| 2 | Validated | All items validated and available |
| 3 | Paid | Payment processed successfully |
| 4 | Processing | Being fulfilled |
| 5 | Completed | Successfully completed |
| 6 | Cancelled | Cancelled |
| 7 | PaymentPending | Waiting for online payment gateway |
| 8 | Returned | Items returned (defined in code, not seeded) |
Domain Events
| Event | Raised By | Payload |
|---|---|---|
OrderCreatedDomainEvent |
Order constructor | Order |
OrderPaidDomainEvent |
MarkAsPaid, CompletePayment |
Order |
OrderCompletedDomainEvent |
MarkAsCompleted |
Order |
OrderPaymentPendingDomainEvent |
MarkAsPaymentPending |
Order |
OrderCancelledDomainEvent |
Cancel |
Order, Reason |
OrderReturnedDomainEvent |
ProcessReturn |
Order |
OrderExchangedDomainEvent |
CreateExchangeCommandHandler | ReturnOrder, NewOrder |
Database Schema
Table: orders
| Column | Type | Nullable | Description |
|---|---|---|---|
id |
uuid | NOT NULL (PK) | Order ID (generated in code) |
shop_id |
uuid | NOT NULL | Shop tenant ID |
customer_id |
uuid | nullable | Customer ID |
table_id |
uuid | nullable | Table ID (dine-in) |
status_id |
int | NOT NULL | FK to order_statuses |
total_amount |
decimal(18,2) | NOT NULL | Order total |
notes |
varchar(2000) | nullable | Notes |
discount_amount |
decimal(18,2) | NOT NULL | Discount amount |
discount_type |
varchar(50) | nullable | Discount type |
discount_reference |
varchar(255) | nullable | Promotion/coupon ref |
payment_method |
varchar(50) | nullable | Payment method used |
transaction_id |
varchar(255) | nullable | External transaction ID |
amount_tendered |
decimal(18,2) | nullable | Amount customer paid |
change_amount |
decimal(18,2) | nullable | Change returned |
is_return |
boolean | NOT NULL (default false) | Return order flag |
return_reason |
varchar(1000) | nullable | Return reason |
returned_at |
timestamp with time zone | nullable | Return timestamp |
original_order_id |
uuid | nullable | Original order (returns) |
created_at |
timestamp with time zone | NOT NULL | Creation time |
updated_at |
timestamp with time zone | nullable | Last update time |
Indexes:
ix_orders_shop_idonshop_idix_orders_customer_idoncustomer_idix_orders_status_idonstatus_idix_orders_created_atoncreated_atix_orders_original_order_idonoriginal_order_id
Note: Dapper queries reference o.staff_id, o.staff_name, o.completed_at, o.vertical columns that are NOT defined in the EF entity configuration. These queries will fail at runtime unless the columns exist in the database via manual migration.
Table: order_items (Owned by Order)
| Column | Type | Nullable | Description |
|---|---|---|---|
id |
uuid | NOT NULL (PK) | Item ID |
order_id |
uuid | NOT NULL (FK) | Parent order |
product_id |
uuid | NOT NULL | Product from catalog |
product_name |
varchar(255) | NOT NULL | Product name snapshot |
product_type |
varchar(50) | NOT NULL | "Physical"/"Service"/"PreparedFood" |
quantity |
int | NOT NULL | Quantity |
unit_price |
decimal(18,2) | NOT NULL | Unit price |
status |
varchar(50) | NOT NULL | "Pending"/"Completed"/"Failed" |
track_inventory |
boolean | NOT NULL (default true) | Auto inventory deduction |
metadata |
jsonb | nullable | Additional JSON data |
Table: order_statuses (Lookup/Seed)
| Column | Type | Nullable | Description |
|---|---|---|---|
id |
int | NOT NULL (PK) | Status ID |
name |
varchar(50) | NOT NULL | Status name |
Seeded data: Draft(1), Validated(2), Paid(3), Processing(4), Completed(5), Cancelled(6), PaymentPending(7)
Note: Returned(8) is defined in code but NOT included in seed data.
Migrations
| Migration | Date | Description |
|---|---|---|
20260117175742_InitialOrder |
2026-01-17 | Initial schema: orders, order_items, order_statuses (1-6) |
20260305004928_AddTableIdAndDiscountFields |
2026-03-05 | Added table_id, discount fields |
20260306175520_PhaseTwo |
2026-03-06 | Added payment fields, return fields, track_inventory, PaymentPending(7) status, original_order_id index |
Strategy Pattern (Line Item Processing)
| Strategy | SupportedType | Validate | Execute |
|---|---|---|---|
RetailStrategy |
"Physical" | Checks inventory availability via InventoryServiceClient | Deducts stock via InventoryServiceClient |
FnbStrategy |
"PreparedFood" | Always returns true (no pre-check) | Creates kitchen ticket via FnbEngineClient + deducts recipe ingredients via InventoryServiceClient |
ServiceStrategy |
"Service" | Checks availability via BookingServiceClient (parses metadata for StartTime/DurationMinutes) | Creates appointment via BookingServiceClient |
Interface: ILineItemStrategy (Domain layer) with SupportedType, ValidateAsync, ExecuteAsync
Factory: StrategyFactory resolves strategy by product type (case-insensitive)
Multi-Tenant Security
EF Core Global Query Filter
Orderentity filtered by_shopId == tenantProvider.GetCurrentShopId()- Bypassed when:
_tenantProvideris null,ShouldBypassTenantFilter()returns true, orGetCurrentShopId()returns null
PostgreSQL RLS (Defense-in-Depth)
TenantMiddlewaresetsSET LOCAL app.current_shop_idandapp.current_merchant_idsession variables- Runs after authentication middleware
- Skipped for service-to-service calls (
X-Service-Call: internal) and admin users
Tenant Provider
HttpContextTenantProviderextracts tenant context from:- JWT claim
shop_id - HTTP header
X-Shop-Id(POS fallback)
- JWT claim
- Admin detection via
roleclaims: "admin", "system", "superadmin" OrderTenantProviderAdapterbridgesITenantProvider(API) toIOrderTenantProvider(Infrastructure)
Dependencies (Cross-Service Communication)
HTTP Clients (with Polly retry + circuit breaker)
| Client | Target Service | Default URL | Timeout | Usage |
|---|---|---|---|---|
CatalogServiceClient |
Catalog Service | http://catalog-service-net:8080 |
10s | Get product details |
InventoryServiceClient |
Inventory Service | http://inventory-service-net:8080 |
10s | Check/deduct stock, deduct by ID |
FnbEngineClient |
F&B Engine | http://fnb-engine-net:8080 |
10s | Kitchen tickets, recipe lookup |
BookingServiceClient |
Booking Service | http://booking-service-net:8080 |
10s | Availability check, create appointment |
WalletServiceClient |
Wallet Service | http://wallet-service-net:8080 |
15s | Create online payment (VNPay/Momo) |
Polly Policies: Retry 3x with exponential backoff (2^attempt seconds), Circuit breaker (5 failures, 30s break)
MediatR Pipeline
Request -> LoggingBehavior -> ValidatorBehavior -> TransactionBehavior -> Handler
- LoggingBehavior: Logs request name and elapsed time (Stopwatch)
- ValidatorBehavior: Runs all registered FluentValidation validators, throws
ValidationExceptionon failure - TransactionBehavior: Auto-wraps Commands in DB transaction (skips Queries by name convention
*Query), usesExecutionStrategyfor retry-safe transactions
Configuration
appsettings.json
| Key | Description | Default |
|---|---|---|
ConnectionStrings:DefaultConnection |
PostgreSQL connection string | Neon PostgreSQL |
Redis:ConnectionString |
Redis for SignalR backplane | localhost:6379 |
SignalR:KeepAliveInterval |
SignalR keep-alive (seconds) | 15 |
SignalR:ClientTimeoutInterval |
Client timeout (seconds) | 30 |
SignalR:StatefulReconnectBufferSize |
Reconnect buffer size | 32768 |
SignalR:EnableMessagePack |
Enable MessagePack protocol | true |
Jwt:Authority |
JWT issuer authority | http://localhost:5001 |
Jwt:Secret |
JWT secret key | (configured) |
AllowedOrigins |
CORS allowed origins | localhost:3000, 5173, 5000 |
Services:CatalogService |
Catalog service URL | http://catalog-service-net:8080 |
Services:InventoryService |
Inventory service URL | http://inventory-service-net:8080 |
Services:BookingService |
Booking service URL | http://booking-service-net:8080 |
Services:FnbEngine |
F&B engine URL | http://fnb-engine-net:8080 |
Services:WalletService |
Wallet service URL | http://wallet-service-net:8080 |
NuGet Dependencies (API)
| Package | Version |
|---|---|
| MediatR | 12.4.1 |
| FluentValidation | 11.11.0 |
| FluentValidation.DependencyInjectionExtensions | 11.11.0 |
| Microsoft.AspNetCore.Authentication.JwtBearer | 10.0.1 |
| Swashbuckle.AspNetCore | 7.2.0 |
| AspNetCore.HealthChecks.NpgSql | 8.0.2 |
| AspNetCore.HealthChecks.Redis | 8.0.1 |
| Hellang.Middleware.ProblemDetails | 6.5.1 |
| Serilog.AspNetCore | 8.0.3 |
| Microsoft.AspNetCore.SignalR.StackExchangeRedis | 9.0.0 |
| Microsoft.AspNetCore.SignalR.Protocols.MessagePack | 9.0.0 |
| Microsoft.EntityFrameworkCore.Design | 10.0.0 |
Tests
Unit Tests (tests/OrderService.UnitTests/)
| Test Class | Tests |
|---|---|
OrderAggregateTests |
CreateOrder_WithValidShopId_ShouldStartInDraftStatus, AddItem_InDraftStatus_ShouldRecalculateTotalAmount, MarkAsValidated_WithoutItems_ShouldThrowDomainException, FullLifecycle_DraftToCompleted_ShouldUpdateStatusSequentially, Cancel_CompletedOrder_ShouldThrowDomainException |
Functional Tests (tests/OrderService.FunctionalTests/)
| Test Class | Description |
|---|---|
HealthChecksControllerTests |
Health endpoint tests |
OrdersControllerTests |
API endpoint integration tests |
CustomWebApplicationFactory |
Test server setup with InMemory DB |
TestAuthHandler |
Fake auth handler for tests |
Known Issues / Gaps
-
Missing DB columns: Dapper queries in
GetStaffPerformanceQueryreferenceo.staff_id,o.staff_name,o.completed_atcolumns, andGetRevenueAnalyticsQueryreferenceso.verticalcolumn. These are NOT defined in Order entity or EF configurations -- queries will fail at runtime unless columns exist via manual migration. -
Returned status not seeded:
OrderStatus.Returned(Id=8) is defined in code but NOT included in theOrderStatusEntityTypeConfigurationseed data. Return orders will fail to load if status is queried via JOIN toorder_statuses. -
Exchange item names:
CreateExchangeCommandHandlerhardcodes product name as "Exchange Item" and type as "Physical" for new items in exchanges -- these should be resolved from the catalog or passed by the frontend. -
No [Authorize] on controllers:
OrdersController,AdminOrdersController, andReportsControllerdo not have[Authorize]attributes. Authentication is configured in Program.cs but not enforced at controller level (only the SignalR hub has[Authorize]). -
Template remnants: Build artifacts contain references to "MyService" namespace (from
_template_dot_net), indicating the service was scaffolded from the template.