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

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 ItemType from Enumeration.FromValue<ItemType>(ItemTypeId). Creates InventoryItem with 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). Calls item.StockIn() which increments quantity and creates an InventoryTransaction(type=In). Raises StockChangedDomainEvent.
  • 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, creates InventoryTransaction(type=Out). Raises StockChangedDomainEvent. 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, creates InventoryTransaction(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, creates InventoryTransaction(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 creates InventoryTransaction(type=Adjustment) with diff. Raises StockChangedDomainEvent. 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, creates InventoryTransaction(type=Wastage). Raises StockChangedDomainEvent. Returns false if not found.
  • Validator: None (domain validation only).

StocktakeCommand

  • Input: ShopId (Guid), Items (List<StocktakeItem>) where StocktakeItem(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 via SaveEntitiesAsync. Returns false if not found.
  • Validator: None.

DeductInventoryCommand

  • Input: ShopId (Guid), ReferenceId (Guid), ReferenceType (string), Reason (string), Items (List<DeductionItem>) where DeductionItem(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 via item.StockOut(). Each item failure is isolated (continues with remaining). Saves all changes in one transaction via SaveEntitiesAsync.
  • 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 with IsLowStock=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:

  1. InventoryItem(productId, shopId, reorderLevel=10) -- For finished goods linked to catalog product. Sets ItemType=FinishedGood, Unit="pcs", Quantity=0.
  2. 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 on id
  • ix_inventory_product_id -- INDEX on product_id
  • ix_inventory_shop_id -- INDEX on shop_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 on id
  • IX_inventory_transactions_inventory_item_id -- INDEX on inventory_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:

  1. EF Core Global Query Filter (InventoryContext.OnModelCreating):

    • Filters InventoryItem by ShopId == currentShopId from IInventoryTenantProvider
    • Bypassed for service-to-service calls (X-Service-Call: internal header) and admin/superadmin roles
  2. PostgreSQL Session Variables (TenantMiddleware):

    • Sets app.current_shop_id and app.current_merchant_id via SET LOCAL for defense-in-depth
    • Skipped for service calls and admin users

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.Infrastructure
  • InventoryService.Infrastructure -> InventoryService.Domain
  • InventoryService.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 / Production
  • DATABASE_URL -- Fallback connection string
  • Jwt:Authority -- IAM IdentityServer URL (default: http://localhost:5001)

Middleware Pipeline Order

  1. UseSerilogRequestLogging()
  2. UseProblemDetails() (RFC 7807)
  3. UseSwagger() (Development only)
  4. UseCors() (AllowAny)
  5. UseRouting()
  6. UseAuthentication() (JWT Bearer via IAM IdentityServer OIDC)
  7. UseAuthorization()
  8. UseTenantMiddleware() (after auth -- sets PG session variables for RLS)
  9. MapHealthChecks() (/health, /health/live, /health/ready)
  10. MapControllers()
  11. Auto-apply EF Core migrations on startup

MediatR Pipeline Behaviors (in order)

  1. LoggingBehavior -- Logs request name, elapsed time via Stopwatch
  2. ValidatorBehavior -- Runs FluentValidation validators, throws ValidationException on failure
  3. TransactionBehavior -- Wraps Commands in DB transaction (skips Queries by name suffix), uses ExecutionStrategy for 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