From ee7d0e529b9fbfd5d9582b0fc7f498f83f3ca917 Mon Sep 17 00:00:00 2001 From: Aditya Siregar Date: Wed, 13 Aug 2025 23:36:31 +0700 Subject: [PATCH] update order status --- internal/contract/inventory_contract.go | 48 ++++ internal/handler/inventory_handler.go | 109 ++++++++ internal/models/inventory.go | 64 ++++- internal/processor/inventory_processor.go | 203 +++++++++++++- internal/processor/order_processor.go | 10 +- internal/repository/inventory_repository.go | 295 +++++++++++++++++++- internal/repository/order_repository.go | 15 + internal/router/router.go | 2 + internal/service/inventory_service.go | 135 ++++++++- 9 files changed, 846 insertions(+), 35 deletions(-) diff --git a/internal/contract/inventory_contract.go b/internal/contract/inventory_contract.go index c329252..15daa87 100644 --- a/internal/contract/inventory_contract.go +++ b/internal/contract/inventory_contract.go @@ -67,3 +67,51 @@ type InventoryAdjustmentResponse struct { Reason string `json:"reason"` AdjustedAt time.Time `json:"adjusted_at"` } + +// Inventory Report Contracts +type InventoryReportSummaryResponse struct { + TotalProducts int `json:"total_products"` + TotalIngredients int `json:"total_ingredients"` + TotalValue float64 `json:"total_value"` + LowStockProducts int `json:"low_stock_products"` + LowStockIngredients int `json:"low_stock_ingredients"` + ZeroStockProducts int `json:"zero_stock_products"` + ZeroStockIngredients int `json:"zero_stock_ingredients"` + OutletID string `json:"outlet_id"` + OutletName string `json:"outlet_name"` + GeneratedAt string `json:"generated_at"` +} + +type InventoryReportDetailResponse struct { + Summary *InventoryReportSummaryResponse `json:"summary"` + Products []*InventoryProductDetailResponse `json:"products"` + Ingredients []*InventoryIngredientDetailResponse `json:"ingredients"` +} + +type InventoryProductDetailResponse struct { + ID string `json:"id"` + ProductID string `json:"product_id"` + ProductName string `json:"product_name"` + CategoryName string `json:"category_name"` + Quantity int `json:"quantity"` + ReorderLevel int `json:"reorder_level"` + UnitCost float64 `json:"unit_cost"` + TotalValue float64 `json:"total_value"` + IsLowStock bool `json:"is_low_stock"` + IsZeroStock bool `json:"is_zero_stock"` + UpdatedAt string `json:"updated_at"` +} + +type InventoryIngredientDetailResponse struct { + ID string `json:"id"` + IngredientID string `json:"ingredient_id"` + IngredientName string `json:"ingredient_name"` + UnitName string `json:"unit_name"` + Quantity int `json:"quantity"` + ReorderLevel int `json:"reorder_level"` + UnitCost float64 `json:"unit_cost"` + TotalValue float64 `json:"total_value"` + IsLowStock bool `json:"is_low_stock"` + IsZeroStock bool `json:"is_zero_stock"` + UpdatedAt string `json:"updated_at"` +} diff --git a/internal/handler/inventory_handler.go b/internal/handler/inventory_handler.go index 51aee97..d92dc8e 100644 --- a/internal/handler/inventory_handler.go +++ b/internal/handler/inventory_handler.go @@ -7,6 +7,7 @@ import ( "apskel-pos-be/internal/constants" "apskel-pos-be/internal/contract" "apskel-pos-be/internal/logger" + "apskel-pos-be/internal/models" "apskel-pos-be/internal/service" "apskel-pos-be/internal/util" "apskel-pos-be/internal/validator" @@ -277,3 +278,111 @@ func (h *InventoryHandler) GetZeroStockItems(c *gin.Context) { util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::GetZeroStockItems") } + +// GetInventoryReportSummary returns summary statistics for inventory report +func (h *InventoryHandler) GetInventoryReportSummary(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + outletIDStr := c.Param("outlet_id") + outletID, err := uuid.Parse(outletIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetInventoryReportSummary -> Invalid outlet ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::GetInventoryReportSummary") + return + } + + summary, err := h.inventoryService.GetInventoryReportSummary(ctx, outletID, contextInfo.OrganizationID) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetInventoryReportSummary -> Failed to get inventory report summary from service") + responseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{responseError}), "InventoryHandler::GetInventoryReportSummary") + return + } + + response := contract.BuildSuccessResponse(summary) + util.HandleResponse(c.Writer, c.Request, response, "InventoryHandler::GetInventoryReportSummary") +} + +// GetInventoryReportDetails returns detailed inventory report with products and ingredients +func (h *InventoryHandler) GetInventoryReportDetails(c *gin.Context) { + ctx := c.Request.Context() + contextInfo := appcontext.FromGinContext(ctx) + + // Parse query parameters + filter := &models.InventoryReportFilter{} + + // Parse outlet_id (required) + if outletIDStr := c.Query("outlet_id"); outletIDStr != "" { + outletID, err := uuid.Parse(outletIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetInventoryReportDetails -> Invalid outlet ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid outlet ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::GetInventoryReportDetails") + return + } + filter.OutletID = &outletID + } else { + logger.FromContext(ctx).Error("InventoryHandler::GetInventoryReportDetails -> Missing outlet_id parameter") + validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "outlet_id is required") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::GetInventoryReportDetails") + return + } + + // Parse category_id (optional) + if categoryIDStr := c.Query("category_id"); categoryIDStr != "" { + categoryID, err := uuid.Parse(categoryIDStr) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetInventoryReportDetails -> Invalid category ID") + validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid category ID") + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::GetInventoryReportDetails") + return + } + filter.CategoryID = &categoryID + } + + // Parse show_low_stock (optional) + if showLowStockStr := c.Query("show_low_stock"); showLowStockStr != "" { + if showLowStock, err := strconv.ParseBool(showLowStockStr); err == nil { + filter.ShowLowStock = &showLowStock + } + } + + // Parse show_zero_stock (optional) + if showZeroStockStr := c.Query("show_zero_stock"); showZeroStockStr != "" { + if showZeroStock, err := strconv.ParseBool(showZeroStockStr); err == nil { + filter.ShowZeroStock = &showZeroStock + } + } + + // Parse search (optional) + if search := c.Query("search"); search != "" { + filter.Search = &search + } + + // Parse limit (optional) + if limitStr := c.Query("limit"); limitStr != "" { + if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 { + filter.Limit = &limit + } + } + + // Parse offset (optional) + if offsetStr := c.Query("offset"); offsetStr != "" { + if offset, err := strconv.Atoi(offsetStr); err == nil && offset >= 0 { + filter.Offset = &offset + } + } + + report, err := h.inventoryService.GetInventoryReportDetails(ctx, filter, contextInfo.OrganizationID) + if err != nil { + logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetInventoryReportDetails -> Failed to get inventory report details from service") + responseError := contract.NewResponseError(constants.InternalServerErrorCode, constants.RequestEntity, err.Error()) + util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{responseError}), "InventoryHandler::GetInventoryReportDetails") + return + } + + response := contract.BuildSuccessResponse(report) + util.HandleResponse(c.Writer, c.Request, response, "InventoryHandler::GetInventoryReportDetails") +} diff --git a/internal/models/inventory.go b/internal/models/inventory.go index 668acd1..c14ab8c 100644 --- a/internal/models/inventory.go +++ b/internal/models/inventory.go @@ -28,8 +28,10 @@ type UpdateInventoryRequest struct { } type InventoryAdjustmentRequest struct { - Delta int - Reason string + ProductID uuid.UUID + OutletID uuid.UUID + Delta int + Reason string } type InventoryResponse struct { @@ -58,3 +60,61 @@ func (i *Inventory) AdjustQuantity(delta int) int { } return newQuantity } + +// Inventory Report Models +type InventoryReportSummary struct { + TotalProducts int `json:"total_products"` + TotalIngredients int `json:"total_ingredients"` + TotalValue float64 `json:"total_value"` + LowStockProducts int `json:"low_stock_products"` + LowStockIngredients int `json:"low_stock_ingredients"` + ZeroStockProducts int `json:"zero_stock_products"` + ZeroStockIngredients int `json:"zero_stock_ingredients"` + OutletID uuid.UUID `json:"outlet_id"` + OutletName string `json:"outlet_name"` + GeneratedAt time.Time `json:"generated_at"` +} + +type InventoryReportDetail struct { + Summary *InventoryReportSummary `json:"summary"` + Products []*InventoryProductDetail `json:"products"` + Ingredients []*InventoryIngredientDetail `json:"ingredients"` +} + +type InventoryProductDetail struct { + ID uuid.UUID `json:"id"` + ProductID uuid.UUID `json:"product_id"` + ProductName string `json:"product_name"` + CategoryName string `json:"category_name"` + Quantity int `json:"quantity"` + ReorderLevel int `json:"reorder_level"` + UnitCost float64 `json:"unit_cost"` + TotalValue float64 `json:"total_value"` + IsLowStock bool `json:"is_low_stock"` + IsZeroStock bool `json:"is_zero_stock"` + UpdatedAt time.Time `json:"updated_at"` +} + +type InventoryIngredientDetail struct { + ID uuid.UUID `json:"id"` + IngredientID uuid.UUID `json:"ingredient_id"` + IngredientName string `json:"ingredient_name"` + UnitName string `json:"unit_name"` + Quantity int `json:"quantity"` + ReorderLevel int `json:"reorder_level"` + UnitCost float64 `json:"unit_cost"` + TotalValue float64 `json:"total_value"` + IsLowStock bool `json:"is_low_stock"` + IsZeroStock bool `json:"is_zero_stock"` + UpdatedAt time.Time `json:"updated_at"` +} + +type InventoryReportFilter struct { + OutletID *uuid.UUID `json:"outlet_id"` + CategoryID *uuid.UUID `json:"category_id"` + ShowLowStock *bool `json:"show_low_stock"` + ShowZeroStock *bool `json:"show_zero_stock"` + Search *string `json:"search"` + Limit *int `json:"limit"` + Offset *int `json:"offset"` +} diff --git a/internal/processor/inventory_processor.go b/internal/processor/inventory_processor.go index 3c37a7f..9a5aea3 100644 --- a/internal/processor/inventory_processor.go +++ b/internal/processor/inventory_processor.go @@ -12,14 +12,21 @@ import ( ) type InventoryProcessor interface { - CreateInventory(ctx context.Context, req *models.CreateInventoryRequest) (*models.InventoryResponse, error) - UpdateInventory(ctx context.Context, id uuid.UUID, req *models.UpdateInventoryRequest) (*models.InventoryResponse, error) - DeleteInventory(ctx context.Context, id uuid.UUID) error - GetInventoryByID(ctx context.Context, id uuid.UUID) (*models.InventoryResponse, error) - ListInventory(ctx context.Context, filters map[string]interface{}, page, limit int) ([]models.InventoryResponse, int, error) - AdjustInventory(ctx context.Context, productID, outletID uuid.UUID, req *models.InventoryAdjustmentRequest) (*models.InventoryResponse, error) - GetLowStockItems(ctx context.Context, outletID uuid.UUID) ([]models.InventoryResponse, error) - GetZeroStockItems(ctx context.Context, outletID uuid.UUID) ([]models.InventoryResponse, error) + Create(ctx context.Context, req *models.CreateInventoryRequest, organizationID uuid.UUID) (*models.InventoryResponse, error) + GetByID(ctx context.Context, id, organizationID uuid.UUID) (*models.InventoryResponse, error) + GetByProductAndOutlet(ctx context.Context, productID, outletID, organizationID uuid.UUID) (*models.InventoryResponse, error) + GetByOutlet(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error) + GetByProduct(ctx context.Context, productID, organizationID uuid.UUID) ([]*models.InventoryResponse, error) + GetLowStock(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error) + GetZeroStock(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error) + Update(ctx context.Context, id uuid.UUID, req *models.UpdateInventoryRequest, organizationID uuid.UUID) (*models.InventoryResponse, error) + Delete(ctx context.Context, id, organizationID uuid.UUID) error + List(ctx context.Context, filters map[string]interface{}, limit, offset int, organizationID uuid.UUID) ([]*models.InventoryResponse, int64, error) + AdjustQuantity(ctx context.Context, productID, outletID, organizationID uuid.UUID, delta int) (*models.InventoryResponse, error) + SetQuantity(ctx context.Context, productID, outletID, organizationID uuid.UUID, quantity int) (*models.InventoryResponse, error) + UpdateReorderLevel(ctx context.Context, id uuid.UUID, reorderLevel int, organizationID uuid.UUID) error + GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID) (*models.InventoryReportSummary, error) + GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter, organizationID uuid.UUID) (*models.InventoryReportDetail, error) } type InventoryProcessorImpl struct { @@ -40,7 +47,8 @@ func NewInventoryProcessorImpl( } } -func (p *InventoryProcessorImpl) CreateInventory(ctx context.Context, req *models.CreateInventoryRequest) (*models.InventoryResponse, error) { +// Create creates a new inventory record +func (p *InventoryProcessorImpl) Create(ctx context.Context, req *models.CreateInventoryRequest, organizationID uuid.UUID) (*models.InventoryResponse, error) { _, err := p.productRepo.GetByID(ctx, req.ProductID) if err != nil { return nil, fmt.Errorf("invalid product: %w", err) @@ -77,7 +85,8 @@ func (p *InventoryProcessorImpl) CreateInventory(ctx context.Context, req *model return response, nil } -func (p *InventoryProcessorImpl) UpdateInventory(ctx context.Context, id uuid.UUID, req *models.UpdateInventoryRequest) (*models.InventoryResponse, error) { +// Update updates an existing inventory record +func (p *InventoryProcessorImpl) Update(ctx context.Context, id uuid.UUID, req *models.UpdateInventoryRequest, organizationID uuid.UUID) (*models.InventoryResponse, error) { // Get existing inventory existingInventory, err := p.inventoryRepo.GetByID(ctx, id) if err != nil { @@ -103,7 +112,8 @@ func (p *InventoryProcessorImpl) UpdateInventory(ctx context.Context, id uuid.UU return response, nil } -func (p *InventoryProcessorImpl) DeleteInventory(ctx context.Context, id uuid.UUID) error { +// Delete deletes an inventory record +func (p *InventoryProcessorImpl) Delete(ctx context.Context, id uuid.UUID, organizationID uuid.UUID) error { // Check if inventory exists _, err := p.inventoryRepo.GetByID(ctx, id) if err != nil { @@ -118,6 +128,177 @@ func (p *InventoryProcessorImpl) DeleteInventory(ctx context.Context, id uuid.UU return nil } +// GetByID retrieves an inventory record by ID +func (p *InventoryProcessorImpl) GetByID(ctx context.Context, id, organizationID uuid.UUID) (*models.InventoryResponse, error) { + inventory, err := p.inventoryRepo.GetWithRelations(ctx, id) + if err != nil { + return nil, fmt.Errorf("inventory not found: %w", err) + } + + response := mappers.InventoryEntityToResponse(inventory) + return response, nil +} + +// GetByProductAndOutlet retrieves inventory by product and outlet +func (p *InventoryProcessorImpl) GetByProductAndOutlet(ctx context.Context, productID, outletID, organizationID uuid.UUID) (*models.InventoryResponse, error) { + inventory, err := p.inventoryRepo.GetByProductAndOutlet(ctx, productID, outletID) + if err != nil { + return nil, fmt.Errorf("inventory not found: %w", err) + } + + response := mappers.InventoryEntityToResponse(inventory) + return response, nil +} + +// GetByOutlet retrieves all inventory records for a specific outlet +func (p *InventoryProcessorImpl) GetByOutlet(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error) { + inventories, err := p.inventoryRepo.GetByOutlet(ctx, outletID) + if err != nil { + return nil, fmt.Errorf("failed to get inventory by outlet: %w", err) + } + + var responses []*models.InventoryResponse + for _, inventory := range inventories { + response := mappers.InventoryEntityToResponse(inventory) + responses = append(responses, response) + } + + return responses, nil +} + +// GetByProduct retrieves all inventory records for a specific product +func (p *InventoryProcessorImpl) GetByProduct(ctx context.Context, productID, organizationID uuid.UUID) ([]*models.InventoryResponse, error) { + inventories, err := p.inventoryRepo.GetByProduct(ctx, productID) + if err != nil { + return nil, fmt.Errorf("failed to get inventory by product: %w", err) + } + + var responses []*models.InventoryResponse + for _, inventory := range inventories { + response := mappers.InventoryEntityToResponse(inventory) + responses = append(responses, response) + } + + return responses, nil +} + +// List retrieves inventory records with filtering and pagination +func (p *InventoryProcessorImpl) List(ctx context.Context, filters map[string]interface{}, limit, offset int, organizationID uuid.UUID) ([]*models.InventoryResponse, int64, error) { + inventories, totalCount, err := p.inventoryRepo.List(ctx, filters, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to list inventory: %w", err) + } + + var responses []*models.InventoryResponse + for _, inventory := range inventories { + response := mappers.InventoryEntityToResponse(inventory) + responses = append(responses, response) + } + + return responses, totalCount, nil +} + +// AdjustQuantity adjusts the quantity of an inventory item +func (p *InventoryProcessorImpl) AdjustQuantity(ctx context.Context, productID, outletID, organizationID uuid.UUID, delta int) (*models.InventoryResponse, error) { + inventory, err := p.inventoryRepo.AdjustQuantity(ctx, productID, outletID, delta) + if err != nil { + return nil, fmt.Errorf("failed to adjust inventory quantity: %w", err) + } + + response := mappers.InventoryEntityToResponse(inventory) + return response, nil +} + +// SetQuantity sets the quantity of an inventory item +func (p *InventoryProcessorImpl) SetQuantity(ctx context.Context, productID, outletID, organizationID uuid.UUID, quantity int) (*models.InventoryResponse, error) { + inventory, err := p.inventoryRepo.SetQuantity(ctx, productID, outletID, quantity) + if err != nil { + return nil, fmt.Errorf("failed to set inventory quantity: %w", err) + } + + response := mappers.InventoryEntityToResponse(inventory) + return response, nil +} + +// UpdateReorderLevel updates the reorder level of an inventory item +func (p *InventoryProcessorImpl) UpdateReorderLevel(ctx context.Context, id uuid.UUID, reorderLevel int, organizationID uuid.UUID) error { + return p.inventoryRepo.UpdateReorderLevel(ctx, id, reorderLevel) +} + +// GetLowStock retrieves low stock inventory items for a specific outlet +func (p *InventoryProcessorImpl) GetLowStock(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error) { + inventories, err := p.inventoryRepo.GetLowStock(ctx, outletID) + if err != nil { + return nil, fmt.Errorf("failed to get low stock inventory: %w", err) + } + + var responses []*models.InventoryResponse + for _, inventory := range inventories { + response := mappers.InventoryEntityToResponse(inventory) + responses = append(responses, response) + } + + return responses, nil +} + +// GetZeroStock retrieves zero stock inventory items for a specific outlet +func (p *InventoryProcessorImpl) GetZeroStock(ctx context.Context, outletID, organizationID uuid.UUID) ([]*models.InventoryResponse, error) { + inventories, err := p.inventoryRepo.GetZeroStock(ctx, outletID) + if err != nil { + return nil, fmt.Errorf("failed to get zero stock inventory: %w", err) + } + + var responses []*models.InventoryResponse + for _, inventory := range inventories { + response := mappers.InventoryEntityToResponse(inventory) + responses = append(responses, response) + } + + return responses, nil +} + +// GetInventoryReportSummary returns summary statistics for inventory report +func (p *InventoryProcessorImpl) GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID) (*models.InventoryReportSummary, error) { + // Verify outlet belongs to organization + outlet, err := p.outletRepo.GetByID(ctx, outletID) + if err != nil { + return nil, fmt.Errorf("outlet not found: %w", err) + } + if outlet.OrganizationID != organizationID { + return nil, fmt.Errorf("outlet does not belong to the organization") + } + + summary, err := p.inventoryRepo.GetInventoryReportSummary(ctx, outletID) + if err != nil { + return nil, fmt.Errorf("failed to get inventory report summary: %w", err) + } + + return summary, nil +} + +// GetInventoryReportDetails returns detailed inventory report with products and ingredients +func (p *InventoryProcessorImpl) GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter, organizationID uuid.UUID) (*models.InventoryReportDetail, error) { + if filter.OutletID == nil { + return nil, fmt.Errorf("outlet_id is required for inventory report") + } + + // Verify outlet belongs to organization + outlet, err := p.outletRepo.GetByID(ctx, *filter.OutletID) + if err != nil { + return nil, fmt.Errorf("outlet not found: %w", err) + } + if outlet.OrganizationID != organizationID { + return nil, fmt.Errorf("outlet does not belong to the organization") + } + + report, err := p.inventoryRepo.GetInventoryReportDetails(ctx, filter) + if err != nil { + return nil, fmt.Errorf("failed to get inventory report details: %w", err) + } + + return report, nil +} + func (p *InventoryProcessorImpl) GetInventoryByID(ctx context.Context, id uuid.UUID) (*models.InventoryResponse, error) { inventoryEntity, err := p.inventoryRepo.GetWithRelations(ctx, id) if err != nil { diff --git a/internal/processor/order_processor.go b/internal/processor/order_processor.go index 69977a5..25ea2a9 100644 --- a/internal/processor/order_processor.go +++ b/internal/processor/order_processor.go @@ -34,6 +34,7 @@ type OrderRepository interface { GetByID(ctx context.Context, id uuid.UUID) (*entities.Order, error) GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.Order, error) Update(ctx context.Context, order *entities.Order) error + UpdateStatusSuccess(ctx context.Context, id uuid.UUID, orderStatus entities.OrderStatus, paymentStatus entities.PaymentStatus) error Delete(ctx context.Context, id uuid.UUID) error List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Order, int64, error) GetByOrderNumber(ctx context.Context, orderNumber string) (*entities.Order, error) @@ -458,12 +459,13 @@ func (p *OrderProcessorImpl) UpdateOrder(ctx context.Context, id uuid.UUID, req } } - // Update order - if err := p.orderRepo.Update(ctx, order); err != nil { + order.Status = entities.OrderStatusCompleted + order.PaymentStatus = entities.PaymentStatusCompleted + + if err := p.orderRepo.UpdateStatusSuccess(ctx, order.ID, order.Status, order.PaymentStatus); err != nil { return nil, fmt.Errorf("failed to update order: %w", err) } - // Get updated order with relations orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, id) if err != nil { return nil, fmt.Errorf("failed to retrieve updated order: %w", err) @@ -858,7 +860,7 @@ func (p *OrderProcessorImpl) updateOrderStatus(ctx context.Context, orderID uuid PaymentStatus: entities.PaymentStatusCompleted, } - if err := p.orderRepo.Update(ctx, orderUpdate); err != nil { + if err := p.orderRepo.UpdateStatusSuccess(ctx, orderID, orderUpdate.Status, orderUpdate.PaymentStatus); err != nil { return fmt.Errorf("failed to update order status: %w", err) } diff --git a/internal/repository/inventory_repository.go b/internal/repository/inventory_repository.go index 943e067..d2ee96d 100644 --- a/internal/repository/inventory_repository.go +++ b/internal/repository/inventory_repository.go @@ -4,8 +4,10 @@ import ( "context" "errors" "fmt" + "time" "apskel-pos-be/internal/entities" + "apskel-pos-be/internal/models" "github.com/google/uuid" "gorm.io/gorm" @@ -31,6 +33,8 @@ type InventoryRepository interface { BulkUpdate(ctx context.Context, inventoryItems []*entities.Inventory) error BulkAdjustQuantity(ctx context.Context, adjustments map[uuid.UUID]int, outletID uuid.UUID) error GetTotalValueByOutlet(ctx context.Context, outletID uuid.UUID) (float64, error) + GetInventoryReportSummary(ctx context.Context, outletID uuid.UUID) (*models.InventoryReportSummary, error) + GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter) (*models.InventoryReportDetail, error) } type InventoryRepositoryImpl struct { @@ -281,7 +285,7 @@ func (r *InventoryRepositoryImpl) BulkUpdate(ctx context.Context, inventoryItems if len(inventoryItems) == 0 { return nil } - + // Use GORM's transaction for bulk updates return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { for _, inventory := range inventoryItems { @@ -326,12 +330,295 @@ func (r *InventoryRepositoryImpl) BulkAdjustQuantity(ctx context.Context, adjust func (r *InventoryRepositoryImpl) GetTotalValueByOutlet(ctx context.Context, outletID uuid.UUID) (float64, error) { var totalValue float64 - err := r.db.WithContext(ctx). + if err := r.db.WithContext(ctx). Table("inventory"). Select("SUM(inventory.quantity * products.cost)"). Joins("JOIN products ON inventory.product_id = products.id"). Where("inventory.outlet_id = ?", outletID). - Scan(&totalValue).Error + Scan(&totalValue).Error; err != nil { + return 0, fmt.Errorf("failed to get total value: %w", err) + } - return totalValue, err + return totalValue, nil +} + +// GetInventoryReportSummary returns summary statistics for inventory report +func (r *InventoryRepositoryImpl) GetInventoryReportSummary(ctx context.Context, outletID uuid.UUID) (*models.InventoryReportSummary, error) { + var summary models.InventoryReportSummary + summary.OutletID = outletID + summary.GeneratedAt = time.Now() + + // Get outlet name + var outlet entities.Outlet + if err := r.db.WithContext(ctx).Select("name").First(&outlet, "id = ?", outletID).Error; err != nil { + return nil, fmt.Errorf("failed to get outlet name: %w", err) + } + summary.OutletName = outlet.Name + + // Get total products count + var totalProducts int64 + if err := r.db.WithContext(ctx).Model(&entities.Inventory{}). + Joins("JOIN products ON inventory.product_id = products.id"). + Where("inventory.outlet_id = ? AND products.has_ingredients = false", outletID). + Count(&totalProducts).Error; err != nil { + return nil, fmt.Errorf("failed to count total products: %w", err) + } + summary.TotalProducts = int(totalProducts) + + // Get total ingredients count + var totalIngredients int64 + if err := r.db.WithContext(ctx).Model(&entities.Inventory{}). + Joins("JOIN ingredients ON inventory.product_id = ingredients.id"). + Where("inventory.outlet_id = ?", outletID). + Count(&totalIngredients).Error; err != nil { + return nil, fmt.Errorf("failed to count total ingredients: %w", err) + } + summary.TotalIngredients = int(totalIngredients) + + // Get low stock products count + var lowStockProducts int64 + if err := r.db.WithContext(ctx).Model(&entities.Inventory{}). + Joins("JOIN products ON inventory.product_id = products.id"). + Where("inventory.outlet_id = ? AND products.has_ingredients = false AND inventory.quantity <= inventory.reorder_level AND inventory.quantity > 0", outletID). + Count(&lowStockProducts).Error; err != nil { + return nil, fmt.Errorf("failed to count low stock products: %w", err) + } + summary.LowStockProducts = int(lowStockProducts) + + // Get low stock ingredients count + var lowStockIngredients int64 + if err := r.db.WithContext(ctx).Model(&entities.Inventory{}). + Joins("JOIN ingredients ON inventory.product_id = ingredients.id"). + Where("inventory.outlet_id = ? AND inventory.quantity <= inventory.reorder_level AND inventory.quantity > 0", outletID). + Count(&lowStockIngredients).Error; err != nil { + return nil, fmt.Errorf("failed to count low stock ingredients: %w", err) + } + summary.LowStockIngredients = int(lowStockIngredients) + + // Get zero stock products count + var zeroStockProducts int64 + if err := r.db.WithContext(ctx).Model(&entities.Inventory{}). + Joins("JOIN products ON inventory.product_id = products.id"). + Where("inventory.outlet_id = ? AND products.has_ingredients = false AND inventory.quantity = 0", outletID). + Count(&zeroStockProducts).Error; err != nil { + return nil, fmt.Errorf("failed to count zero stock products: %w", err) + } + summary.ZeroStockProducts = int(zeroStockProducts) + + // Get zero stock ingredients count + var zeroStockIngredients int64 + if err := r.db.WithContext(ctx).Model(&entities.Inventory{}). + Joins("JOIN ingredients ON inventory.product_id = ingredients.id"). + Where("inventory.outlet_id = ? AND inventory.quantity = 0", outletID). + Count(&zeroStockIngredients).Error; err != nil { + return nil, fmt.Errorf("failed to count zero stock ingredients: %w", err) + } + summary.ZeroStockIngredients = int(zeroStockIngredients) + + // Get total value + totalValue, err := r.GetTotalValueByOutlet(ctx, outletID) + if err != nil { + return nil, fmt.Errorf("failed to get total value: %w", err) + } + summary.TotalValue = totalValue + + return &summary, nil +} + +// GetInventoryReportDetails returns detailed inventory report with products and ingredients +func (r *InventoryRepositoryImpl) GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter) (*models.InventoryReportDetail, error) { + report := &models.InventoryReportDetail{} + + // Get summary + if filter.OutletID != nil { + summary, err := r.GetInventoryReportSummary(ctx, *filter.OutletID) + if err != nil { + return nil, fmt.Errorf("failed to get report summary: %w", err) + } + report.Summary = summary + } + + // Get products details + products, err := r.getInventoryProductsDetails(ctx, filter) + if err != nil { + return nil, fmt.Errorf("failed to get products details: %w", err) + } + report.Products = products + + // Get ingredients details + ingredients, err := r.getInventoryIngredientsDetails(ctx, filter) + if err != nil { + return nil, fmt.Errorf("failed to get ingredients details: %w", err) + } + report.Ingredients = ingredients + + return report, nil +} + +// getInventoryProductsDetails retrieves detailed product inventory information +func (r *InventoryRepositoryImpl) getInventoryProductsDetails(ctx context.Context, filter *models.InventoryReportFilter) ([]*models.InventoryProductDetail, error) { + query := r.db.WithContext(ctx).Table("inventory"). + Select(` + inventory.id, + inventory.product_id, + products.name as product_name, + categories.name as category_name, + inventory.quantity, + inventory.reorder_level, + COALESCE(product_variants.cost, products.cost) as unit_cost, + (COALESCE(product_variants.cost, products.cost) * inventory.quantity) as total_value, + inventory.updated_at + `). + Joins("JOIN products ON inventory.product_id = products.id"). + Joins("LEFT JOIN categories ON products.category_id = categories.id"). + Joins("LEFT JOIN product_variants ON products.id = product_variants.product_id"). + Where("inventory.outlet_id = ? AND products.has_ingredients = false", filter.OutletID) + + // Apply filters + if filter.CategoryID != nil { + query = query.Where("products.category_id = ?", *filter.CategoryID) + } + if filter.ShowLowStock != nil && *filter.ShowLowStock { + query = query.Where("inventory.quantity <= inventory.reorder_level AND inventory.quantity > 0") + } + if filter.ShowZeroStock != nil && *filter.ShowZeroStock { + query = query.Where("inventory.quantity = 0") + } + if filter.Search != nil && *filter.Search != "" { + searchTerm := "%" + *filter.Search + "%" + query = query.Where("products.name ILIKE ? OR categories.name ILIKE ?", searchTerm, searchTerm) + } + + // Apply pagination + if filter.Limit != nil { + query = query.Limit(*filter.Limit) + } + if filter.Offset != nil { + query = query.Offset(*filter.Offset) + } + + query = query.Order("products.name ASC") + + var results []struct { + ID uuid.UUID + ProductID uuid.UUID + ProductName string + CategoryName *string + Quantity int + ReorderLevel int + UnitCost float64 + TotalValue float64 + UpdatedAt time.Time + } + + if err := query.Find(&results).Error; err != nil { + return nil, err + } + + var products []*models.InventoryProductDetail + for _, result := range results { + categoryName := "" + if result.CategoryName != nil { + categoryName = *result.CategoryName + } + + product := &models.InventoryProductDetail{ + ID: result.ID, + ProductID: result.ProductID, + ProductName: result.ProductName, + CategoryName: categoryName, + Quantity: result.Quantity, + ReorderLevel: result.ReorderLevel, + UnitCost: result.UnitCost, + TotalValue: result.TotalValue, + IsLowStock: result.Quantity <= result.ReorderLevel && result.Quantity > 0, + IsZeroStock: result.Quantity == 0, + UpdatedAt: result.UpdatedAt, + } + products = append(products, product) + } + + return products, nil +} + +// getInventoryIngredientsDetails retrieves detailed ingredient inventory information +func (r *InventoryRepositoryImpl) getInventoryIngredientsDetails(ctx context.Context, filter *models.InventoryReportFilter) ([]*models.InventoryIngredientDetail, error) { + query := r.db.WithContext(ctx).Table("inventory"). + Select(` + inventory.id, + inventory.product_id as ingredient_id, + ingredients.name as ingredient_name, + units.name as unit_name, + inventory.quantity, + inventory.reorder_level, + ingredients.cost as unit_cost, + (ingredients.cost * inventory.quantity) as total_value, + inventory.updated_at + `). + Joins("JOIN ingredients ON inventory.product_id = ingredients.id"). + Joins("LEFT JOIN units ON ingredients.unit_id = units.id"). + Where("inventory.outlet_id = ?", filter.OutletID) + + // Apply filters + if filter.ShowLowStock != nil && *filter.ShowLowStock { + query = query.Where("inventory.quantity <= inventory.reorder_level AND inventory.quantity > 0") + } + if filter.ShowZeroStock != nil && *filter.ShowZeroStock { + query = query.Where("inventory.quantity = 0") + } + if filter.Search != nil && *filter.Search != "" { + searchTerm := "%" + *filter.Search + "%" + query = query.Where("ingredients.name ILIKE ? OR units.name ILIKE ?", searchTerm, searchTerm) + } + + // Apply pagination + if filter.Limit != nil { + query = query.Limit(*filter.Limit) + } + if filter.Offset != nil { + query = query.Offset(*filter.Offset) + } + + query = query.Order("ingredients.name ASC") + + var results []struct { + ID uuid.UUID + IngredientID uuid.UUID + IngredientName string + UnitName *string + Quantity int + ReorderLevel int + UnitCost float64 + TotalValue float64 + UpdatedAt time.Time + } + + if err := query.Find(&results).Error; err != nil { + return nil, err + } + + var ingredients []*models.InventoryIngredientDetail + for _, result := range results { + unitName := "" + if result.UnitName != nil { + unitName = *result.UnitName + } + + ingredient := &models.InventoryIngredientDetail{ + ID: result.ID, + IngredientID: result.IngredientID, + IngredientName: result.IngredientName, + UnitName: unitName, + Quantity: result.Quantity, + ReorderLevel: result.ReorderLevel, + UnitCost: result.UnitCost, + TotalValue: result.TotalValue, + IsLowStock: result.Quantity <= result.ReorderLevel && result.Quantity > 0, + IsZeroStock: result.Quantity == 0, + UpdatedAt: result.UpdatedAt, + } + ingredients = append(ingredients, ingredient) + } + + return ingredients, nil } diff --git a/internal/repository/order_repository.go b/internal/repository/order_repository.go index 0bd7eee..6cf5b4a 100644 --- a/internal/repository/order_repository.go +++ b/internal/repository/order_repository.go @@ -74,6 +74,21 @@ func (r *OrderRepositoryImpl) Update(ctx context.Context, order *entities.Order) return r.db.WithContext(ctx).Save(order).Error } +func (r *OrderRepositoryImpl) UpdateStatusSuccess( + ctx context.Context, + id uuid.UUID, + orderStatus entities.OrderStatus, + paymentStatus entities.PaymentStatus, +) error { + return r.db.WithContext(ctx). + Model(&entities.Order{}). + Where("id = ?", id). + Updates(map[string]interface{}{ + "order_status": orderStatus, + "payment_status": paymentStatus, + }).Error +} + func (r *OrderRepositoryImpl) Delete(ctx context.Context, id uuid.UUID) error { return r.db.WithContext(ctx).Delete(&entities.Order{}, "id = ?", id).Error } diff --git a/internal/router/router.go b/internal/router/router.go index c46e966..96271cb 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -205,6 +205,8 @@ func (r *Router) addAppRoutes(rg *gin.Engine) { inventory.POST("/adjust", r.inventoryHandler.AdjustInventory) inventory.GET("/low-stock/:outlet_id", r.inventoryHandler.GetLowStockItems) inventory.GET("/zero-stock/:outlet_id", r.inventoryHandler.GetZeroStockItems) + inventory.GET("/report/summary/:outlet_id", r.inventoryHandler.GetInventoryReportSummary) + inventory.GET("/report/details", r.inventoryHandler.GetInventoryReportDetails) } orders := protected.Group("/orders") diff --git a/internal/service/inventory_service.go b/internal/service/inventory_service.go index 5a8284e..1efcf0f 100644 --- a/internal/service/inventory_service.go +++ b/internal/service/inventory_service.go @@ -2,10 +2,12 @@ package service import ( "context" + "time" "apskel-pos-be/internal/appcontext" "apskel-pos-be/internal/constants" "apskel-pos-be/internal/contract" + "apskel-pos-be/internal/models" "apskel-pos-be/internal/processor" "apskel-pos-be/internal/transformer" @@ -21,6 +23,8 @@ type InventoryService interface { AdjustInventory(ctx context.Context, req *contract.AdjustInventoryRequest) *contract.Response GetLowStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response GetZeroStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response + GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID) (*contract.InventoryReportSummaryResponse, error) + GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter, organizationID uuid.UUID) (*contract.InventoryReportDetailResponse, error) } type InventoryServiceImpl struct { @@ -36,7 +40,7 @@ func NewInventoryService(inventoryProcessor processor.InventoryProcessor) *Inven func (s *InventoryServiceImpl) CreateInventory(ctx context.Context, apctx *appcontext.ContextInfo, req *contract.CreateInventoryRequest) *contract.Response { modelReq := transformer.CreateInventoryRequestToModel(req) - inventoryResponse, err := s.inventoryProcessor.CreateInventory(ctx, modelReq) + inventoryResponse, err := s.inventoryProcessor.Create(ctx, modelReq, apctx.OrganizationID) if err != nil { errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) @@ -49,7 +53,7 @@ func (s *InventoryServiceImpl) CreateInventory(ctx context.Context, apctx *appco func (s *InventoryServiceImpl) UpdateInventory(ctx context.Context, id uuid.UUID, req *contract.UpdateInventoryRequest) *contract.Response { modelReq := transformer.UpdateInventoryRequestToModel(req) - inventoryResponse, err := s.inventoryProcessor.UpdateInventory(ctx, id, modelReq) + inventoryResponse, err := s.inventoryProcessor.Update(ctx, id, modelReq, uuid.Nil) // TODO: Get organizationID from context if err != nil { errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) @@ -60,7 +64,7 @@ func (s *InventoryServiceImpl) UpdateInventory(ctx context.Context, id uuid.UUID } func (s *InventoryServiceImpl) DeleteInventory(ctx context.Context, id uuid.UUID) *contract.Response { - err := s.inventoryProcessor.DeleteInventory(ctx, id) + err := s.inventoryProcessor.Delete(ctx, id, uuid.Nil) // TODO: Get organizationID from context if err != nil { errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) @@ -72,7 +76,7 @@ func (s *InventoryServiceImpl) DeleteInventory(ctx context.Context, id uuid.UUID } func (s *InventoryServiceImpl) GetInventoryByID(ctx context.Context, id uuid.UUID) *contract.Response { - inventoryResponse, err := s.inventoryProcessor.GetInventoryByID(ctx, id) + inventoryResponse, err := s.inventoryProcessor.GetByID(ctx, id, uuid.Nil) // TODO: Get organizationID from context if err != nil { errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) @@ -104,24 +108,30 @@ func (s *InventoryServiceImpl) ListInventory(ctx context.Context, req *contract. filters["search"] = req.Search } - inventory, totalCount, err := s.inventoryProcessor.ListInventory(ctx, filters, req.Page, req.Limit) + inventory, totalCount, err := s.inventoryProcessor.List(ctx, filters, req.Limit, req.Page, uuid.Nil) // TODO: Get organizationID from context if err != nil { errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) } + // Convert from []*models.InventoryResponse to []models.InventoryResponse + var inventoryResponses []models.InventoryResponse + for _, inv := range inventory { + inventoryResponses = append(inventoryResponses, *inv) + } + // Convert to contract responses - contractResponses := transformer.InventoryToResponses(inventory) + contractResponses := transformer.InventoryToResponses(inventoryResponses) // Calculate total pages - totalPages := totalCount / req.Limit - if totalCount%req.Limit > 0 { + totalPages := int(totalCount) / req.Limit + if int(totalCount)%req.Limit > 0 { totalPages++ } listResponse := &contract.ListInventoryResponse{ Inventory: contractResponses, - TotalCount: totalCount, + TotalCount: int(totalCount), Page: req.Page, Limit: req.Limit, TotalPages: totalPages, @@ -133,7 +143,7 @@ func (s *InventoryServiceImpl) ListInventory(ctx context.Context, req *contract. func (s *InventoryServiceImpl) AdjustInventory(ctx context.Context, req *contract.AdjustInventoryRequest) *contract.Response { modelReq := transformer.AdjustInventoryRequestToModel(req) - inventoryResponse, err := s.inventoryProcessor.AdjustInventory(ctx, req.ProductID, req.OutletID, modelReq) + inventoryResponse, err := s.inventoryProcessor.AdjustQuantity(ctx, modelReq.ProductID, modelReq.OutletID, uuid.Nil, modelReq.Delta) // TODO: Get organizationID from context if err != nil { errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) @@ -144,23 +154,120 @@ func (s *InventoryServiceImpl) AdjustInventory(ctx context.Context, req *contrac } func (s *InventoryServiceImpl) GetLowStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response { - inventory, err := s.inventoryProcessor.GetLowStockItems(ctx, outletID) + inventory, err := s.inventoryProcessor.GetLowStock(ctx, outletID, uuid.Nil) // TODO: Get organizationID from context if err != nil { errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) } - contractResponses := transformer.InventoryToResponses(inventory) + // Convert from []*models.InventoryResponse to []models.InventoryResponse + var inventoryResponses []models.InventoryResponse + for _, inv := range inventory { + inventoryResponses = append(inventoryResponses, *inv) + } + + contractResponses := transformer.InventoryToResponses(inventoryResponses) return contract.BuildSuccessResponse(contractResponses) } func (s *InventoryServiceImpl) GetZeroStockItems(ctx context.Context, outletID uuid.UUID) *contract.Response { - inventory, err := s.inventoryProcessor.GetZeroStockItems(ctx, outletID) + inventory, err := s.inventoryProcessor.GetZeroStock(ctx, outletID, uuid.Nil) // TODO: Get organizationID from context if err != nil { errorResp := contract.NewResponseError(constants.InternalServerErrorCode, constants.InventoryServiceEntity, err.Error()) return contract.BuildErrorResponse([]*contract.ResponseError{errorResp}) } - contractResponses := transformer.InventoryToResponses(inventory) + // Convert from []*models.InventoryResponse to []models.InventoryResponse + var inventoryResponses []models.InventoryResponse + for _, inv := range inventory { + inventoryResponses = append(inventoryResponses, *inv) + } + + contractResponses := transformer.InventoryToResponses(inventoryResponses) return contract.BuildSuccessResponse(contractResponses) } + +// GetInventoryReportSummary returns summary statistics for inventory report +func (s *InventoryServiceImpl) GetInventoryReportSummary(ctx context.Context, outletID, organizationID uuid.UUID) (*contract.InventoryReportSummaryResponse, error) { + summary, err := s.inventoryProcessor.GetInventoryReportSummary(ctx, outletID, organizationID) + if err != nil { + return nil, err + } + + return &contract.InventoryReportSummaryResponse{ + TotalProducts: summary.TotalProducts, + TotalIngredients: summary.TotalIngredients, + TotalValue: summary.TotalValue, + LowStockProducts: summary.LowStockProducts, + LowStockIngredients: summary.LowStockIngredients, + ZeroStockProducts: summary.ZeroStockProducts, + ZeroStockIngredients: summary.ZeroStockIngredients, + OutletID: summary.OutletID.String(), + OutletName: summary.OutletName, + GeneratedAt: summary.GeneratedAt.Format(time.RFC3339), + }, nil +} + +// GetInventoryReportDetails returns detailed inventory report with products and ingredients +func (s *InventoryServiceImpl) GetInventoryReportDetails(ctx context.Context, filter *models.InventoryReportFilter, organizationID uuid.UUID) (*contract.InventoryReportDetailResponse, error) { + report, err := s.inventoryProcessor.GetInventoryReportDetails(ctx, filter, organizationID) + if err != nil { + return nil, err + } + + response := &contract.InventoryReportDetailResponse{} + + // Transform summary + if report.Summary != nil { + response.Summary = &contract.InventoryReportSummaryResponse{ + TotalProducts: report.Summary.TotalProducts, + TotalIngredients: report.Summary.TotalIngredients, + TotalValue: report.Summary.TotalValue, + LowStockProducts: report.Summary.LowStockProducts, + LowStockIngredients: report.Summary.LowStockIngredients, + ZeroStockProducts: report.Summary.ZeroStockProducts, + ZeroStockIngredients: report.Summary.ZeroStockIngredients, + OutletID: report.Summary.OutletID.String(), + OutletName: report.Summary.OutletName, + GeneratedAt: report.Summary.GeneratedAt.Format(time.RFC3339), + } + } + + // Transform products + response.Products = make([]*contract.InventoryProductDetailResponse, len(report.Products)) + for i, product := range report.Products { + response.Products[i] = &contract.InventoryProductDetailResponse{ + ID: product.ID.String(), + ProductID: product.ProductID.String(), + ProductName: product.ProductName, + CategoryName: product.CategoryName, + Quantity: product.Quantity, + ReorderLevel: product.ReorderLevel, + UnitCost: product.UnitCost, + TotalValue: product.TotalValue, + IsLowStock: product.IsLowStock, + IsZeroStock: product.IsZeroStock, + UpdatedAt: product.UpdatedAt.Format(time.RFC3339), + } + } + + // Transform ingredients + response.Ingredients = make([]*contract.InventoryIngredientDetailResponse, len(report.Ingredients)) + for i, ingredient := range report.Ingredients { + response.Ingredients[i] = &contract.InventoryIngredientDetailResponse{ + ID: ingredient.ID.String(), + IngredientID: ingredient.IngredientID.String(), + IngredientName: ingredient.IngredientName, + UnitName: ingredient.UnitName, + Quantity: ingredient.Quantity, + ReorderLevel: ingredient.ReorderLevel, + UnitCost: ingredient.UnitCost, + TotalValue: ingredient.TotalValue, + IsLowStock: ingredient.IsLowStock, + IsZeroStock: ingredient.IsZeroStock, + UpdatedAt: ingredient.UpdatedAt.Format(time.RFC3339), + } + } + + return response, nil +}