32 KiB
Inventory Service - Service Documentation
Auto-generated from source code audit on 2026-03-13
Overview
Service: inventory-service-net
Framework: .NET 10.0 (C# 14), ASP.NET Core Web API
Architecture: Clean Architecture + CQRS (MediatR 12.4.1)
Database: PostgreSQL (Neon) via EF Core 10 + Npgsql 10
Port: 5018 (Development)
Base Route: api/v1/inventory
Database Name: inventory_service
The Inventory Service manages stock levels, reservations, transactions, and low-stock alerts for shop inventory items. It supports both catalog-linked finished goods and standalone raw materials/consumables. Features multi-tenant row-level security via global query filters and PostgreSQL session variables, idempotent bulk deduction for kitchen ticket integration, and a full audit trail of all inventory movements.
API Endpoints
| Method | Route | Description | Request Body / Query | Response |
|---|---|---|---|---|
| GET | /api/v1/inventory |
Get inventory by shop (paginated) | ?shopId={guid}&skip=0&take=50 |
ApiResponse<PagedResult<InventoryItemDto>> |
| GET | /api/v1/inventory/{productId} |
Get stock level by product+shop | ?shopId={guid} |
ApiResponse<InventoryItemDto> |
| POST | /api/v1/inventory/stock/bulk |
Bulk stock check (POS cart validation) | BulkStockCheckRequest |
ApiResponse<IReadOnlyList<StockLevelDto>> |
| PUT | /api/v1/inventory/alerts |
Set low stock alert threshold | SetLowStockAlertRequest |
ApiResponse<bool> |
| POST | /api/v1/inventory/items |
Create new inventory item | CreateInventoryItemRequest |
ApiResponse<Guid> (201 Created) |
| POST | /api/v1/inventory/stock-in |
Stock in (add inventory) | StockInRequest |
ApiResponse<Guid> |
| POST | /api/v1/inventory/stock-out |
Stock out (by productId+shopId) | StockOutRequest |
ApiResponse<bool> |
| POST | /api/v1/inventory/stock-out-by-id |
Stock out by inventory item ID | StockOutByIdRequest |
ApiResponse<bool> |
| POST | /api/v1/inventory/reserve |
Reserve stock for order | ReserveStockRequest |
ApiResponse<bool> |
| POST | /api/v1/inventory/release |
Release stock reservation | ReleaseReservationRequest |
ApiResponse<bool> |
| POST | /api/v1/inventory/adjust |
Manual stock adjustment | AdjustStockRequest |
ApiResponse<bool> |
| POST | /api/v1/inventory/wastage |
Record wastage/shrinkage | RecordWastageRequest |
ApiResponse<bool> |
| POST | /api/v1/inventory/stocktake |
Perform stocktake (inventory count) | StocktakeRequest |
ApiResponse<StocktakeResult> |
| POST | /api/v1/inventory/deduct |
Bulk deduct (kitchen ticket served) | DeductInventoryRequest |
ApiResponse<DeductInventoryResult> |
| DELETE | /api/v1/inventory/items/{id} |
Delete inventory item | - | ApiResponse<bool> |
| GET | /api/v1/inventory/transactions |
Get transaction history | ?inventoryItemId={guid}&shopId={guid}&skip=0&take=50 |
ApiResponse<PagedResult<InventoryTransactionDto>> |
| GET | /api/v1/inventory/low-stock |
Get low stock items | ?shopId={guid}&skip=0&take=50 |
ApiResponse<PagedResult<InventoryItemDto>> |
| GET | /health |
Health check (all) | - | Healthy/Unhealthy |
| GET | /health/live |
Liveness probe | - | Healthy |
| GET | /health/ready |
Readiness probe (DB check) | - | Healthy/Unhealthy |
Commands
CreateInventoryItemCommand
- Input:
ShopId (Guid),Name (string),ItemTypeId (int),Unit (string),CostPerUnit (decimal),InitialQuantity (int),ReorderLevel (int),SupplierName (string?),ExpiryDate (DateTime?) - Returns:
Guid(new item ID) - Logic: Resolves
ItemTypefromEnumeration.FromValue<ItemType>(ItemTypeId). CreatesInventoryItemwith full details (raw materials / consumables constructor). Persists via repository + UnitOfWork. - Validator:
CreateInventoryItemCommandValidator-- ShopId NotEmpty, Name NotEmpty + MaxLength(200), ItemTypeId InclusiveBetween(1,3), Unit NotEmpty + MaxLength(20), CostPerUnit >= 0, InitialQuantity >= 0, ReorderLevel >= 0.
StockInCommand
- Input:
ProductId (Guid),ShopId (Guid),Amount (int),Notes (string?),ReferenceId (Guid?),InvoiceImageUrl (string?),UnitCost (decimal?) - Returns:
Guid(inventory item ID) - Logic: Finds existing item by ProductId+ShopId, or creates a new
InventoryItem(FinishedGood type). Callsitem.StockIn()which increments quantity and creates anInventoryTransaction(type=In). RaisesStockChangedDomainEvent. - Validator:
StockInCommandValidator-- ProductId NotEmpty, ShopId NotEmpty, Amount > 0.
StockOutCommand
- Input:
ProductId (Guid),ShopId (Guid),Amount (int),Notes (string?),ReferenceId (Guid?) - Returns:
bool - Logic: Finds item by ProductId+ShopId. Calls
item.StockOut()which validates available quantity, decrements quantity, createsInventoryTransaction(type=Out). RaisesStockChangedDomainEvent. Returns false if item not found. - Validator:
StockOutCommandValidator-- ProductId NotEmpty, ShopId NotEmpty, Amount > 0.
StockOutByIdCommand
- Input:
InventoryItemId (Guid),Amount (int),Notes (string?),ReferenceId (Guid?) - Returns:
bool - Logic: Finds item by ID directly (for recipe ingredient deduction). Calls
item.StockOut(). Returns false if not found. - Validator: None (no dedicated validator).
ReserveStockCommand
- Input:
ProductId (Guid),ShopId (Guid),Amount (int),OrderId (Guid) - Returns:
bool - Logic: Finds item by ProductId+ShopId. Calls
item.Reserve()which validates available quantity, increments_reservedQuantity, createsInventoryTransaction(type=Reserve). Returns false if not found. - Validator:
ReserveStockCommandValidator-- ProductId NotEmpty, ShopId NotEmpty, Amount > 0, OrderId NotEmpty.
ReleaseReservationCommand
- Input:
ProductId (Guid),ShopId (Guid),Amount (int),OrderId (Guid) - Returns:
bool - Logic: Finds item by ProductId+ShopId. Calls
item.ReleaseReservation()which validates reserved >= amount, decrements_reservedQuantity, createsInventoryTransaction(type=Release). Returns false if not found. - Validator:
ReleaseReservationCommandValidator-- ProductId NotEmpty, ShopId NotEmpty, Amount > 0, OrderId NotEmpty.
AdjustStockCommand
- Input:
ProductId (Guid),ShopId (Guid),NewQuantity (int),Notes (string) - Returns:
bool - Logic: Finds item by ProductId+ShopId. Calls
item.Adjust()which sets quantity directly and createsInventoryTransaction(type=Adjustment)with diff. RaisesStockChangedDomainEvent. Returns false if not found. - Validator:
AdjustStockCommandValidator-- ProductId NotEmpty, ShopId NotEmpty, NewQuantity >= 0, Notes NotEmpty.
SetLowStockAlertCommand
- Input:
ShopId (Guid),ProductId (Guid),MinimumLevel (int) - Returns:
bool - Logic: Finds item by ProductId+ShopId. Calls
item.SetReorderLevel()which validates >= 0 and updates_reorderLevel. Returns false if not found. - Validator:
SetLowStockAlertCommandValidator-- ShopId NotEmpty, ProductId NotEmpty, MinimumLevel >= 0.
RecordWastageCommand
- Input:
InventoryItemId (Guid),Amount (int),Reason (string),Notes (string?) - Returns:
bool - Logic: Finds item by ID. Calls
item.RecordWastage()which validates amount > 0 and reason non-empty, decrements quantity, createsInventoryTransaction(type=Wastage). RaisesStockChangedDomainEvent. Returns false if not found. - Validator: None (domain validation only).
StocktakeCommand
- Input:
ShopId (Guid),Items (List<StocktakeItem>)whereStocktakeItem(InventoryItemId, CountedQuantity) - Returns:
StocktakeResult(Discrepancies, TotalItemsCounted) - Logic: Iterates items, compares counted vs expected quantity, records discrepancies, calls
item.Adjust()for each. Saves all in one transaction. - Validator: None.
DeleteInventoryItemCommand
- Input:
InventoryItemId (Guid) - Returns:
bool - Logic: Finds item by ID. Calls
repository.Delete(). Saves viaSaveEntitiesAsync. Returns false if not found. - Validator: None.
DeductInventoryCommand
- Input:
ShopId (Guid),ReferenceId (Guid),ReferenceType (string),Reason (string),Items (List<DeductionItem>)whereDeductionItem(InventoryItemId, Amount, Unit, IngredientName) - Returns:
DeductInventoryResult(Success, ItemsDeducted, ItemsSkipped, Items) - Logic: Idempotency check via
IRequestManager-- skips if ReferenceId already processed. Records request. Iterates items: if item not found, skip; if insufficient stock, partially deducts what's available; otherwise full deduction viaitem.StockOut(). Each item failure is isolated (continues with remaining). Saves all changes in one transaction viaSaveEntitiesAsync. - Validator:
DeductInventoryCommandValidator-- ShopId NotEmpty, ReferenceId NotEmpty, ReferenceType NotEmpty, Items NotEmpty, each item: InventoryItemId NotEmpty, Amount > 0.
Queries
GetInventoryByShopQuery
- Input:
ShopId (Guid),Skip (int, default=0),Take (int, default=50) - Returns:
PagedResult<InventoryItemDto> - Logic: Filters by ShopId, orders by ProductId, applies skip/take pagination.
GetStockLevelQuery
- Input:
ProductId (Guid),ShopId (Guid) - Returns:
InventoryItemDto? - Logic: Finds single item by ProductId+ShopId. Returns null if not found.
GetStockLevelsQuery (Bulk)
- Input:
ShopId (Guid),ProductIds (List<Guid>) - Returns:
IReadOnlyList<StockLevelDto> - Logic: Fetches all items matching ShopId and ProductIds. Maps to
StockLevelDto. Products without inventory records return zero stock withIsLowStock=true.
GetTransactionsQuery
- Input:
InventoryItemId (Guid),Skip (int),Take (int) - Returns:
PagedResult<InventoryTransactionDto> - Logic: Loads item with
.Include(Transactions), orders by CreatedAt descending, applies pagination.
GetTransactionsByShopQuery
- Input:
ShopId (Guid),Skip (int),Take (int) - Returns:
PagedResult<InventoryTransactionDto> - Logic: Loads all items for shop with
.Include(Transactions), flattens all transactions, orders by CreatedAt descending, applies pagination.
GetLowStockItemsQuery
- Input:
ShopId (Guid?, optional),Skip (int),Take (int) - Returns:
PagedResult<InventoryItemDto> - Logic: Filters items where
(Quantity - ReservedQuantity) <= ReorderLevel. Optional ShopId filter. Orders by available quantity ascending.
Domain Model
InventoryItem (Aggregate Root)
Entity: InventoryService.Domain.AggregatesModel.InventoryAggregate.InventoryItem
Extends: Entity, implements IAggregateRoot
Private Fields / Public Properties:
| Field | Type | Property | Description |
|---|---|---|---|
_productId |
Guid |
ProductId |
Product ID from Catalog Service |
_shopId |
Guid |
ShopId |
Shop that owns this inventory |
_name |
string? |
Name |
Item name (for raw materials not linked to catalog) |
_itemType |
ItemType |
ItemType |
Enumeration (RawMaterial/FinishedGood/Consumable) |
| - | int |
ItemTypeId |
EF Core mapping for item type (private set) |
_unit |
string |
Unit |
Unit of measure (g, ml, pcs, kg, L) default "pcs" |
_costPerUnit |
decimal |
CostPerUnit |
Purchase price per unit |
_supplierName |
string? |
SupplierName |
Supplier reference |
_expiryDate |
DateTime? |
ExpiryDate |
For perishable items |
_quantity |
int |
Quantity |
Total quantity in stock |
_reservedQuantity |
int |
ReservedQuantity |
Reserved for orders |
| - (computed) | int |
AvailableQuantity |
Quantity - ReservedQuantity |
_reorderLevel |
int |
ReorderLevel |
Low stock alert threshold, default 10 |
_transactions |
List<InventoryTransaction> |
Transactions |
Transaction history (readonly collection) |
_createdAt |
DateTime |
CreatedAt |
Creation timestamp |
_updatedAt |
DateTime? |
UpdatedAt |
Last update timestamp |
Constructors:
InventoryItem(productId, shopId, reorderLevel=10)-- For finished goods linked to catalog product. Sets ItemType=FinishedGood, Unit="pcs", Quantity=0.InventoryItem(shopId, name, itemType, unit, costPerUnit, initialQuantity, reorderLevel, supplierName?, expiryDate?)-- For raw materials/consumables. Generates own ProductId.
Behavior Methods:
| Method | Logic | Domain Event |
|---|---|---|
StockIn(amount, notes?, referenceId?, invoiceImageUrl?, unitCost?) |
Validates amount > 0. Increments _quantity. Creates Transaction(type=In). |
StockChangedDomainEvent |
StockOut(amount, notes?, referenceId?) |
Validates amount > 0 and AvailableQuantity >= amount. Decrements _quantity. Creates Transaction(type=Out, negative qty). |
StockChangedDomainEvent |
Reserve(amount, orderId) |
Validates AvailableQuantity >= amount. Increments _reservedQuantity. Creates Transaction(type=Reserve). |
None |
ReleaseReservation(amount, orderId) |
Validates _reservedQuantity >= amount. Decrements _reservedQuantity. Creates Transaction(type=Release, negative qty). |
None |
SetReorderLevel(reorderLevel) |
Validates >= 0. Updates _reorderLevel. |
None |
Adjust(newQuantity, notes) |
Calculates diff. Sets _quantity = newQuantity. Creates Transaction(type=Adjustment, diff). |
StockChangedDomainEvent |
RecordWastage(amount, reason, notes?) |
Validates amount > 0 and reason non-empty. Decrements _quantity. Creates Transaction(type=Wastage, negative qty). |
StockChangedDomainEvent |
UpdateItemDetails(name?, unit?, costPerUnit?, supplierName?, expiryDate?) |
Updates non-null fields. Validates cost >= 0. | None |
InventoryTransaction (Owned Entity)
Entity: InventoryService.Domain.AggregatesModel.InventoryAggregate.InventoryTransaction
Extends: Entity (NOT an aggregate root -- owned by InventoryItem)
| Field | Type | Property | Description |
|---|---|---|---|
_inventoryItemId |
Guid |
InventoryItemId |
Parent inventory item |
_type |
TransactionType |
Type |
Enumeration (In/Out/Adjustment/Reserve/Release/Wastage) |
| - | int |
TypeId |
EF Core mapping (private set) |
_quantity |
int |
Quantity |
Positive for in/reserve, negative for out/release/wastage |
_referenceId |
Guid? |
ReferenceId |
Order ID, PO ID, etc. |
_notes |
string? |
Notes |
Transaction notes (max 500 chars) |
_invoiceImageUrl |
string? |
InvoiceImageUrl |
Invoice image URL for stock-in (max 1000 chars) |
_unitCost |
decimal? |
UnitCost |
Cost per unit at time of stock-in |
_createdAt |
DateTime |
CreatedAt |
Transaction timestamp |
Enumerations
ItemType (Enumeration pattern):
| Id | Name | Description |
|---|---|---|
| 1 | RawMaterial | Ingredients for production (coffee beans, milk, sugar) |
| 2 | FinishedGood | Ready-to-sell products (Cappuccino, Espresso) |
| 3 | Consumable | Operational supplies (cups, napkins, straws) |
TransactionType (Enumeration pattern):
| Id | Name | Description |
|---|---|---|
| 1 | In | Stock in - receiving inventory |
| 2 | Out | Stock out - releasing inventory |
| 3 | Adjustment | Manual adjustment / stocktake |
| 4 | Reserve | Reserve for order |
| 5 | Release | Release reservation |
| 6 | Wastage | Damaged, expired, spilled items |
Domain Events
| Event | Payload | Raised By |
|---|---|---|
StockChangedDomainEvent |
InventoryItem Item |
StockIn, StockOut, Adjust, RecordWastage |
Domain Exception
DomainException -- thrown for business rule violations (insufficient stock, invalid amounts, empty required fields).
Database Schema
Database: inventory_service (PostgreSQL)
Table: inventory_items
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
uuid |
NOT NULL | - | Primary key (ValueGeneratedNever) |
product_id |
uuid |
NOT NULL | - | Product reference (catalog or self-generated) |
shop_id |
uuid |
NOT NULL | - | Shop owner |
name |
varchar(200) |
NULL | - | Item name (raw materials) |
item_type_id |
int |
NOT NULL | 2 |
Item type (1=RawMaterial, 2=FinishedGood, 3=Consumable) |
unit |
varchar(20) |
NOT NULL | 'pcs' |
Unit of measure |
cost_per_unit |
numeric(18,4) |
NOT NULL | 0 |
Purchase price per unit |
supplier_name |
varchar(200) |
NULL | - | Supplier reference |
expiry_date |
timestamptz |
NULL | - | Expiry date |
quantity |
int |
NOT NULL | - | Total quantity in stock |
reserved_quantity |
int |
NOT NULL | - | Quantity reserved for orders |
reorder_level |
int |
NOT NULL | 10 |
Low stock alert threshold |
updated_at |
timestamptz |
NULL | - | Last update timestamp |
Indexes:
PK_inventory_items-- PRIMARY KEY onidix_inventory_product_id-- INDEX onproduct_idix_inventory_shop_id-- INDEX onshop_id
Note: AvailableQuantity is computed in-memory (Quantity - ReservedQuantity), not stored. CreatedAt, Name, ItemType, Unit, CostPerUnit, SupplierName, ExpiryDate are mapped via private fields and ignored as navigation properties in EF config.
Table: inventory_transactions (Owned by InventoryItem)
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
uuid |
NOT NULL | - | Primary key (ValueGeneratedNever) |
inventory_item_id |
uuid |
NOT NULL | - | FK to inventory_items.id (CASCADE delete) |
type_id |
int |
NOT NULL | - | Transaction type (1-6) |
quantity |
int |
NOT NULL | - | Amount (positive for in, negative for out) |
reference_id |
uuid |
NULL | - | External reference (order ID, etc.) |
notes |
varchar(500) |
NULL | - | Transaction notes |
invoice_image_url |
varchar(1000) |
NULL | - | Invoice image URL |
unit_cost |
numeric(18,4) |
NULL | - | Unit cost at time of transaction |
created_at |
timestamptz |
NOT NULL | - | Transaction timestamp |
Indexes:
PK_inventory_transactions-- PRIMARY KEY onidIX_inventory_transactions_inventory_item_id-- INDEX oninventory_item_id
Table: client_requests (Idempotency)
| Column | Type | Nullable | Default | Description |
|---|---|---|---|---|
id |
uuid |
NOT NULL | - | Request identifier (ReferenceId) |
name |
varchar(200) |
NOT NULL | - | Command type name |
time |
timestamptz |
NOT NULL | - | Request timestamp |
Note: transaction_types and item_types tables were configured as EF seed data tables but are currently ignored in OnModelCreating (modelBuilder.Ignore<TransactionType>() / modelBuilder.Ignore<ItemType>()). The transaction_types table was dropped in migration AddItemTypeAndCostFields. Enumerations are resolved in-memory via the DDD Enumeration pattern.
Migrations History
| Migration | Date | Description |
|---|---|---|
20260117180439_InitialInventory |
2026-01-17 | Creates inventory_items, inventory_transactions, transaction_types tables with seed data |
20260305095121_AddItemTypeAndCostFields |
2026-03-05 | Drops transaction_types table. Adds name, item_type_id, unit, cost_per_unit, supplier_name, expiry_date to items. Adds invoice_image_url, unit_cost to transactions. |
20260306175523_PhaseTwo |
2026-03-06 | Creates client_requests table for idempotency tracking |
Multi-Tenant Architecture
Row-Level Security (RLS)
The service implements two-layer tenant isolation:
-
EF Core Global Query Filter (
InventoryContext.OnModelCreating):- Filters
InventoryItembyShopId == currentShopIdfromIInventoryTenantProvider - Bypassed for service-to-service calls (
X-Service-Call: internalheader) and admin/superadmin roles
- Filters
-
PostgreSQL Session Variables (
TenantMiddleware):- Sets
app.current_shop_idandapp.current_merchant_idviaSET LOCALfor defense-in-depth - Skipped for service calls and admin users
- Sets
Tenant Context Sources
- JWT Claims:
shop_id,merchant_id,sub(user ID),role - HTTP Headers:
X-Shop-Id(fallback for shop ID),X-Service-Call: internal(service-to-service bypass)
Dependencies
NuGet Packages
API Layer (InventoryService.API.csproj):
| Package | Version |
|---|---|
| MediatR | 12.4.1 |
| FluentValidation | 11.11.0 |
| FluentValidation.DependencyInjectionExtensions | 11.11.0 |
| Microsoft.EntityFrameworkCore.Design | 10.0.0 |
| Microsoft.AspNetCore.Authentication.JwtBearer | 10.0.1 |
| Swashbuckle.AspNetCore | 7.2.0 |
| Swashbuckle.AspNetCore.Annotations | 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 |
| Serilog.Sinks.Console | 6.0.0 |
| Serilog.Sinks.Seq | 8.0.0 |
Domain Layer (InventoryService.Domain.csproj):
| Package | Version |
|---|---|
| MediatR.Contracts | 2.0.1 |
Infrastructure Layer (InventoryService.Infrastructure.csproj):
| Package | Version |
|---|---|
| Microsoft.EntityFrameworkCore | 10.0.0 |
| Npgsql.EntityFrameworkCore.PostgreSQL | 10.0.0 |
| Microsoft.EntityFrameworkCore.Tools | 10.0.0 |
| MediatR | 12.4.1 |
| Dapper | 2.1.35 |
| Microsoft.Extensions.Http.Polly | 9.0.0 |
| Polly | 8.5.0 |
| StackExchange.Redis | 2.8.16 |
Project References
InventoryService.API->InventoryService.Domain,InventoryService.InfrastructureInventoryService.Infrastructure->InventoryService.DomainInventoryService.Domain-> None (pure domain, no external dependencies)
Configuration
appsettings.json
{
"ConnectionStrings": {
"DefaultConnection": "<Neon PostgreSQL connection string>"
},
"Redis": {
"ConnectionString": "localhost:6379"
},
"Jwt": {
"Secret": "...",
"Issuer": "goodgo-platform",
"Audience": "goodgo-services",
"AccessTokenExpiryMinutes": 15,
"RefreshTokenExpiryDays": 7
}
}
Environment Variables
ASPNETCORE_ENVIRONMENT-- Development / ProductionDATABASE_URL-- Fallback connection stringJwt:Authority-- IAM IdentityServer URL (default:http://localhost:5001)
Middleware Pipeline Order
UseSerilogRequestLogging()UseProblemDetails()(RFC 7807)UseSwagger()(Development only)UseCors()(AllowAny)UseRouting()UseAuthentication()(JWT Bearer via IAM IdentityServer OIDC)UseAuthorization()UseTenantMiddleware()(after auth -- sets PG session variables for RLS)MapHealthChecks()(/health,/health/live,/health/ready)MapControllers()- Auto-apply EF Core migrations on startup
MediatR Pipeline Behaviors (in order)
LoggingBehavior-- Logs request name, elapsed time via StopwatchValidatorBehavior-- Runs FluentValidation validators, throwsValidationExceptionon failureTransactionBehavior-- Wraps Commands in DB transaction (skips Queries by name suffix), usesExecutionStrategyfor retry
DTOs
Request DTOs
| DTO | Fields |
|---|---|
CreateInventoryItemRequest |
ShopId, Name, ItemTypeId, Unit, CostPerUnit, InitialQuantity(=0), ReorderLevel(=10), SupplierName?, ExpiryDate? |
StockInRequest |
ProductId, ShopId, Amount, Notes?, ReferenceId?, InvoiceImageUrl?, UnitCost? |
StockOutRequest |
ProductId, ShopId, Amount, Notes?, ReferenceId? |
StockOutByIdRequest |
InventoryItemId, Amount, Notes?, ReferenceId? |
ReserveStockRequest |
ProductId, ShopId, Amount, OrderId |
ReleaseReservationRequest |
ProductId, ShopId, Amount, OrderId |
AdjustStockRequest |
ProductId, ShopId, NewQuantity, Notes |
SetLowStockAlertRequest |
ShopId, ProductId, MinimumLevel |
RecordWastageRequest |
InventoryItemId, Amount, Reason, Notes? |
StocktakeRequest |
ShopId, Items(List of {InventoryItemId, CountedQuantity}) |
BulkStockCheckRequest |
ShopId, ProductIds(List) |
DeductInventoryRequest |
ShopId, ReferenceId, ReferenceType, Reason?, Items(List of {InventoryItemId, Amount, Unit, IngredientName}) |
Response DTOs
| DTO | Fields |
|---|---|
InventoryItemDto |
Id, ProductId, ShopId, Name?, ItemType, ItemTypeId, Unit, CostPerUnit, SupplierName?, ExpiryDate?, Quantity, ReservedQuantity, AvailableQuantity, ReorderLevel, UpdatedAt? |
InventoryTransactionDto |
Id, InventoryItemId, TransactionType, Quantity, ReferenceId?, Notes?, InvoiceImageUrl?, UnitCost?, CreatedAt |
StockLevelDto |
ProductId, Available, Reserved, Minimum, IsLowStock |
StocktakeResult |
Discrepancies(List), TotalItemsCounted |
StocktakeDiscrepancy |
InventoryItemId, ItemName?, ExpectedQuantity, CountedQuantity, Difference |
DeductInventoryResult |
Success, ItemsDeducted, ItemsSkipped, Items(List) |
DeductionResultItem |
InventoryItemId, IngredientName, Deducted, Error? |
ApiResponse<T> |
Success, Data?, Error? |
PagedResult<T> |
Items, TotalCount |
Tests
Unit Tests (InventoryService.UnitTests)
File: Application/Commands/DeductInventoryCommandHandlerTests.cs
| Test | Description |
|---|---|
Handle_WithValidCommand_ShouldDeductInventory |
Full deduction of available stock |
Handle_WithInsufficientStock_ShouldReturnPartialDeduction |
Partial deduction when stock < requested |
Handle_WithNonExistentItem_ShouldReturnNotFound |
Item not found returns Success=false |
Handle_WithZeroStockAvailable_ShouldSkipDeduction |
Zero stock skips with error message |
Handle_WithDuplicateRequest_ShouldReturnIdempotentResult |
Duplicate ReferenceId detected and skipped |
Handle_WithMultipleItems_ShouldDeductAll |
Multiple items deducted in single operation |
Handle_WithMixedResults_ShouldDeductFoundItemsAndSkipMissing |
Mix of found and missing items |
Handle_WithItemExceptionDuringDeduction_ShouldContinueWithRemainingItems |
Error resilience -- continues after individual item failure |
Constructor_WithNullRepository_ShouldThrowArgumentNullException |
Null guard |
Constructor_WithNullRequestManager_ShouldThrowArgumentNullException |
Null guard |
Constructor_WithNullLogger_ShouldThrowArgumentNullException |
Null guard |
Handle_ShouldRecordIdempotencyRequest |
Verifies idempotency request is recorded |
Functional Tests (InventoryService.FunctionalTests)
Factory: CustomWebApplicationFactory -- swaps PostgreSQL for InMemoryDatabase
| Test | Description |
|---|---|
GetInventory_WithoutShopId_ShouldReturnBadRequest |
Validates shopId is required |
GetLowStockItems_ShouldReturnOk |
Low stock endpoint returns 200 |
HealthCheck_ShouldReturnHealthy |
Liveness probe returns 200 |
Cross-Service Integration
| Consumer Service | Endpoint Used | Purpose |
|---|---|---|
| fnb-engine-net | POST /api/v1/inventory/deduct |
Bulk ingredient deduction when kitchen ticket is served |
| order-service-net (planned) | POST /api/v1/inventory/reserve / POST /api/v1/inventory/release |
Reserve stock for orders, release on cancellation |
| POS Frontend | POST /api/v1/inventory/stock/bulk |
Cart validation -- check stock availability before order |
| POS Frontend | GET /api/v1/inventory |
Admin inventory management dashboard |
| POS Frontend | GET /api/v1/inventory/transactions |
Transaction history display |
File Structure
services/inventory-service-net/
global.json # SDK: .NET 10.0.101
src/
InventoryService.API/
Program.cs # DI + middleware pipeline + auto-migration
Controllers/
InventoryController.cs # 17 endpoints (1 controller)
Application/
Behaviors/
LoggingBehavior.cs # Request logging with Stopwatch
ValidatorBehavior.cs # FluentValidation pipeline
TransactionBehavior.cs # Auto-transaction for commands
Commands/
InventoryCommands.cs # 11 command records
InventoryCommandHandlers.cs # 10 command handlers
DeductInventory/
DeductInventoryCommand.cs # Bulk deduction command + result records
DeductInventoryCommandHandler.cs # Idempotent bulk deduction handler
Queries/
InventoryQueries.cs # 6 query records
InventoryQueryHandlers.cs # 6 query handlers
Validations/
InventoryValidators.cs # 7 validators
DeductInventoryCommandValidator.cs # Deduct command validator
DTOs/
InventoryDtos.cs # All DTOs, request/response records, ApiResponse<T>
Mappers/
InventoryMapper.cs # Extension methods: ToDto() with fallback type resolution
Infrastructure/Tenant/
ITenantProvider.cs # Tenant context interface
HttpContextTenantProvider.cs # JWT claims + header extraction
InventoryTenantProviderAdapter.cs # API->Infrastructure adapter
Middleware/
TenantMiddleware.cs # PostgreSQL session variable setter for RLS
InventoryService.Domain/
AggregatesModel/InventoryAggregate/
InventoryItem.cs # Aggregate root (344 lines)
InventoryTransaction.cs # Owned entity
IInventoryRepository.cs # Repository interface (13 methods)
ItemType.cs # Enumeration (3 values)
TransactionType.cs # Enumeration (6 values)
Events/
InventoryDomainEvents.cs # StockChangedDomainEvent
Exceptions/
DomainException.cs # Business rule exception
SeedWork/
Entity.cs, IAggregateRoot.cs, IRepository.cs, IUnitOfWork.cs, Enumeration.cs, ValueObject.cs
InventoryService.Infrastructure/
InventoryContext.cs # DbContext + IUnitOfWork + domain event dispatch + multi-tenant filter
DependencyInjection.cs # AddInfrastructure() extension
Repositories/
InventoryRepository.cs # IInventoryRepository implementation
EntityConfigurations/
InventoryItemEntityTypeConfiguration.cs # Fluent API (snake_case, private field mapping, owned transactions)
ItemTypeEntityTypeConfiguration.cs # Seed data config (currently ignored in OnModelCreating)
TransactionTypeEntityTypeConfiguration.cs # Seed data config (currently ignored in OnModelCreating)
Idempotency/
ClientRequest.cs # Idempotency entity
IRequestManager.cs # Interface
RequestManager.cs # Implementation
Migrations/
20260117180439_InitialInventory.cs
20260305095121_AddItemTypeAndCostFields.cs
20260306175523_PhaseTwo.cs
tests/
InventoryService.UnitTests/
Application/Commands/
DeductInventoryCommandHandlerTests.cs # 12 tests
InventoryService.FunctionalTests/
CustomWebApplicationFactory.cs # InMemoryDatabase swap
Controllers/
InventoryControllerTests.cs # 3 tests