// EN: Main controller for Inventory operations. // VI: Controller chính cho các thao tác Inventory. using InventoryService.API.Application.Commands; using InventoryService.API.Application.Commands.DeductInventory; using InventoryService.API.Application.DTOs; using InventoryService.API.Application.Queries; using MediatR; using Microsoft.AspNetCore.Mvc; using Swashbuckle.AspNetCore.Annotations; namespace InventoryService.API.Controllers; /// /// EN: Controller for inventory operations. /// VI: Controller cho các thao tác inventory. /// [ApiController] [Route("api/v1/inventory")] [SwaggerTag("Inventory Management - Stock operations, reservations, and tracking")] public class InventoryController : ControllerBase { private readonly IMediator _mediator; private readonly ILogger _logger; public InventoryController( IMediator mediator, ILogger logger) { _mediator = mediator; _logger = logger; } /// /// EN: Get inventory by shop. /// VI: Lấy inventory theo shop. /// [HttpGet] [SwaggerOperation(Summary = "Get inventory by shop with pagination")] [SwaggerResponse(200, "Inventory items retrieved successfully")] [SwaggerResponse(400, "Invalid request")] public async Task>>> GetInventory( [FromQuery] Guid shopId, [FromQuery] int skip = 0, [FromQuery] int take = 50, CancellationToken ct = default) { if (shopId == Guid.Empty) return BadRequest(ApiResponse>.Fail("Shop ID is required")); var query = new GetInventoryByShopQuery(shopId, skip, take); var result = await _mediator.Send(query, ct); return Ok(ApiResponse>.Ok(result)); } /// /// EN: Get stock level for specific product and shop. /// VI: Lấy mức tồn kho cho product và shop cụ thể. /// [HttpGet("{productId:guid}")] [SwaggerOperation(Summary = "Get stock level by product ID and shop ID")] [SwaggerResponse(200, "Stock level retrieved successfully")] [SwaggerResponse(404, "Inventory item not found")] public async Task>> GetStockLevel( Guid productId, [FromQuery] Guid shopId, CancellationToken ct = default) { var query = new GetStockLevelQuery(productId, shopId); var result = await _mediator.Send(query, ct); if (result == null) return NotFound(ApiResponse.Fail("Inventory item not found")); return Ok(ApiResponse.Ok(result)); } /// /// EN: Create a new inventory item (raw material, consumable, etc.). /// VI: Tạo mặt hàng tồn kho mới (nguyên liệu, vật tư tiêu hao, v.v.). /// [HttpPost("items")] [SwaggerOperation(Summary = "Create a new inventory item")] [SwaggerResponse(201, "Item created successfully")] [SwaggerResponse(400, "Invalid request")] public async Task>> CreateItem( [FromBody] CreateInventoryItemRequest request, CancellationToken ct = default) { try { var command = new CreateInventoryItemCommand( request.ShopId, request.Name, request.ItemTypeId, request.Unit, request.CostPerUnit, request.InitialQuantity, request.ReorderLevel, request.SupplierName, request.ExpiryDate); var itemId = await _mediator.Send(command, ct); return Created($"/api/v1/inventory/{itemId}", ApiResponse.Ok(itemId)); } catch (Exception ex) { _logger.LogError(ex, "Error creating inventory item"); return BadRequest(ApiResponse.Fail(ex.Message)); } } /// /// EN: Stock in operation (add inventory). /// VI: Thao tác nhập kho (thêm inventory). /// [HttpPost("stock-in")] [SwaggerOperation(Summary = "Add stock to inventory")] [SwaggerResponse(200, "Stock added successfully")] [SwaggerResponse(400, "Invalid request")] public async Task>> StockIn( [FromBody] StockInRequest request, CancellationToken ct = default) { try { var command = new StockInCommand( request.ProductId, request.ShopId, request.Amount, request.Notes, request.ReferenceId, request.InvoiceImageUrl, request.UnitCost); var inventoryItemId = await _mediator.Send(command, ct); return Ok(ApiResponse.Ok(inventoryItemId)); } catch (Exception ex) { _logger.LogError(ex, "Error performing stock in"); return BadRequest(ApiResponse.Fail(ex.Message)); } } /// /// EN: Stock out operation (remove inventory). /// VI: Thao tác xuất kho (giảm inventory). /// [HttpPost("stock-out")] [SwaggerOperation(Summary = "Remove stock from inventory")] [SwaggerResponse(200, "Stock removed successfully")] [SwaggerResponse(400, "Invalid request or insufficient stock")] [SwaggerResponse(404, "Inventory item not found")] public async Task>> StockOut( [FromBody] StockOutRequest request, CancellationToken ct = default) { try { var command = new StockOutCommand( request.ProductId, request.ShopId, request.Amount, request.Notes, request.ReferenceId); var result = await _mediator.Send(command, ct); if (!result) return NotFound(ApiResponse.Fail("Inventory item not found")); return Ok(ApiResponse.Ok(result)); } catch (Exception ex) { _logger.LogError(ex, "Error performing stock out"); return BadRequest(ApiResponse.Fail(ex.Message)); } } /// /// EN: Stock out by inventory item ID (for recipe ingredient deduction). /// VI: Xuất kho theo inventory item ID (cho trừ nguyên liệu công thức). /// [HttpPost("stock-out-by-id")] [SwaggerOperation(Summary = "Deduct stock by inventory item ID")] [SwaggerResponse(200, "Stock deducted successfully")] [SwaggerResponse(400, "Invalid request or insufficient stock")] [SwaggerResponse(404, "Inventory item not found")] public async Task>> StockOutById( [FromBody] StockOutByIdRequest request, CancellationToken ct = default) { try { var command = new StockOutByIdCommand( request.InventoryItemId, request.Amount, request.Notes, request.ReferenceId); var result = await _mediator.Send(command, ct); if (!result) return NotFound(ApiResponse.Fail("Inventory item not found")); return Ok(ApiResponse.Ok(result)); } catch (Exception ex) { _logger.LogError(ex, "Error performing stock out by ID"); return BadRequest(ApiResponse.Fail(ex.Message)); } } /// /// EN: Reserve stock for order. /// VI: Đặt trước stock cho order. /// [HttpPost("reserve")] [SwaggerOperation(Summary = "Reserve stock for order")] [SwaggerResponse(200, "Stock reserved successfully")] [SwaggerResponse(400, "Invalid request or insufficient available stock")] [SwaggerResponse(404, "Inventory item not found")] public async Task>> ReserveStock( [FromBody] ReserveStockRequest request, CancellationToken ct = default) { try { var command = new ReserveStockCommand( request.ProductId, request.ShopId, request.Amount, request.OrderId); var result = await _mediator.Send(command, ct); if (!result) return NotFound(ApiResponse.Fail("Inventory item not found")); return Ok(ApiResponse.Ok(result)); } catch (Exception ex) { _logger.LogError(ex, "Error reserving stock"); return BadRequest(ApiResponse.Fail(ex.Message)); } } /// /// EN: Release stock reservation. /// VI: Giải phóng đặt trước stock. /// [HttpPost("release")] [SwaggerOperation(Summary = "Release stock reservation")] [SwaggerResponse(200, "Reservation released successfully")] [SwaggerResponse(400, "Invalid request")] [SwaggerResponse(404, "Inventory item not found")] public async Task>> ReleaseReservation( [FromBody] ReleaseReservationRequest request, CancellationToken ct = default) { try { var command = new ReleaseReservationCommand( request.ProductId, request.ShopId, request.Amount, request.OrderId); var result = await _mediator.Send(command, ct); if (!result) return NotFound(ApiResponse.Fail("Inventory item not found")); return Ok(ApiResponse.Ok(result)); } catch (Exception ex) { _logger.LogError(ex, "Error releasing reservation"); return BadRequest(ApiResponse.Fail(ex.Message)); } } /// /// EN: Adjust stock (manual correction). /// VI: Điều chỉnh stock (sửa thủ công). /// [HttpPost("adjust")] [SwaggerOperation(Summary = "Manually adjust stock quantity")] [SwaggerResponse(200, "Stock adjusted successfully")] [SwaggerResponse(400, "Invalid request")] [SwaggerResponse(404, "Inventory item not found")] public async Task>> AdjustStock( [FromBody] AdjustStockRequest request, CancellationToken ct = default) { try { var command = new AdjustStockCommand( request.ProductId, request.ShopId, request.NewQuantity, request.Notes); var result = await _mediator.Send(command, ct); if (!result) return NotFound(ApiResponse.Fail("Inventory item not found")); return Ok(ApiResponse.Ok(result)); } catch (Exception ex) { _logger.LogError(ex, "Error adjusting stock"); return BadRequest(ApiResponse.Fail(ex.Message)); } } /// /// EN: Record wastage/shrinkage. /// VI: Ghi nhận hao hụt. /// [HttpPost("wastage")] [SwaggerOperation(Summary = "Record wastage/shrinkage for an inventory item")] [SwaggerResponse(200, "Wastage recorded successfully")] [SwaggerResponse(400, "Invalid request")] [SwaggerResponse(404, "Inventory item not found")] public async Task>> RecordWastage( [FromBody] RecordWastageRequest request, CancellationToken ct = default) { try { var command = new RecordWastageCommand( request.InventoryItemId, request.Amount, request.Reason, request.Notes); var result = await _mediator.Send(command, ct); if (!result) return NotFound(ApiResponse.Fail("Inventory item not found")); return Ok(ApiResponse.Ok(result)); } catch (Exception ex) { _logger.LogError(ex, "Error recording wastage"); return BadRequest(ApiResponse.Fail(ex.Message)); } } /// /// EN: Perform stocktake (inventory count). /// VI: Thực hiện kiểm kê (đếm tồn kho). /// [HttpPost("stocktake")] [SwaggerOperation(Summary = "Perform stocktake and return discrepancies")] [SwaggerResponse(200, "Stocktake completed successfully")] [SwaggerResponse(400, "Invalid request")] public async Task>> Stocktake( [FromBody] StocktakeRequest request, CancellationToken ct = default) { try { var command = new StocktakeCommand( request.ShopId, request.Items.Select(i => new StocktakeItem(i.InventoryItemId, i.CountedQuantity)).ToList()); var result = await _mediator.Send(command, ct); return Ok(ApiResponse.Ok(result)); } catch (Exception ex) { _logger.LogError(ex, "Error performing stocktake"); return BadRequest(ApiResponse.Fail(ex.Message)); } } /// /// EN: Delete an inventory item. /// VI: Xóa nguyên liệu khỏi tồn kho. /// [HttpDelete("items/{id:guid}")] [SwaggerOperation(Summary = "Delete an inventory item")] [SwaggerResponse(200, "Item deleted successfully")] [SwaggerResponse(404, "Item not found")] public async Task>> DeleteItem( Guid id, CancellationToken ct = default) { try { var result = await _mediator.Send(new DeleteInventoryItemCommand(id), ct); if (!result) return NotFound(ApiResponse.Fail("Inventory item not found")); return Ok(ApiResponse.Ok(true)); } catch (Exception ex) { _logger.LogError(ex, "Error deleting inventory item"); return BadRequest(ApiResponse.Fail(ex.Message)); } } /// /// EN: Get transaction history by inventory item ID or shop ID. /// VI: Lấy lịch sử transactions theo inventory item ID hoặc shop ID. /// [HttpGet("transactions")] [SwaggerOperation(Summary = "Get transaction history by item or shop")] [SwaggerResponse(200, "Transactions retrieved successfully")] [SwaggerResponse(400, "Invalid request")] public async Task>>> GetTransactions( [FromQuery] Guid? inventoryItemId = null, [FromQuery] Guid? shopId = null, [FromQuery] int skip = 0, [FromQuery] int take = 50, CancellationToken ct = default) { if (shopId.HasValue && shopId.Value != Guid.Empty) { var shopQuery = new GetTransactionsByShopQuery(shopId.Value, skip, take); var shopResult = await _mediator.Send(shopQuery, ct); return Ok(ApiResponse>.Ok(shopResult)); } if (!inventoryItemId.HasValue || inventoryItemId.Value == Guid.Empty) return BadRequest(ApiResponse>.Fail("Either shopId or inventoryItemId is required")); var query = new GetTransactionsQuery(inventoryItemId.Value, skip, take); var result = await _mediator.Send(query, ct); return Ok(ApiResponse>.Ok(result)); } /// /// EN: Bulk deduct inventory items (called by fnb-engine when kitchen ticket is served). /// VI: Trừ kho hàng loạt (được gọi bởi fnb-engine khi phiếu bếp đã phục vụ). /// [HttpPost("deduct")] [SwaggerOperation(Summary = "Bulk deduct inventory items (cross-service: kitchen ticket served)")] [SwaggerResponse(200, "Deduction processed")] [SwaggerResponse(400, "Invalid request")] public async Task>> DeductInventory( [FromBody] DeductInventoryRequest request, CancellationToken ct = default) { try { var command = new DeductInventoryCommand( request.ShopId, request.ReferenceId, request.ReferenceType, request.Reason ?? "Kitchen ticket deduction", request.Items.Select(i => new DeductionItem( i.InventoryItemId, i.Amount, i.Unit, i.IngredientName)).ToList()); var result = await _mediator.Send(command, ct); return Ok(ApiResponse.Ok(result)); } catch (Exception ex) { _logger.LogError(ex, "EN: Error processing inventory deduction / VI: Lỗi xử lý trừ kho"); return BadRequest(ApiResponse.Fail(ex.Message)); } } /// /// EN: Get low stock items. /// VI: Lấy các items stock thấp. /// [HttpGet("low-stock")] [SwaggerOperation(Summary = "Get items with stock at or below reorder level")] [SwaggerResponse(200, "Low stock items retrieved successfully")] public async Task>>> GetLowStockItems( [FromQuery] Guid? shopId = null, [FromQuery] int skip = 0, [FromQuery] int take = 50, CancellationToken ct = default) { var query = new GetLowStockItemsQuery(shopId, skip, take); var result = await _mediator.Send(query, ct); return Ok(ApiResponse>.Ok(result)); } }