package handler import ( "strconv" "time" "apskel-pos-be/internal/appcontext" "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" "github.com/gin-gonic/gin" "github.com/google/uuid" ) type InventoryHandler struct { inventoryService service.InventoryService inventoryValidator validator.InventoryValidator } func NewInventoryHandler( inventoryService service.InventoryService, inventoryValidator validator.InventoryValidator, ) *InventoryHandler { return &InventoryHandler{ inventoryService: inventoryService, inventoryValidator: inventoryValidator, } } func (h *InventoryHandler) CreateInventory(c *gin.Context) { ctx := c.Request.Context() contextInfo := appcontext.FromGinContext(ctx) var req contract.CreateInventoryRequest if err := c.ShouldBindJSON(&req); err != nil { logger.FromContext(c.Request.Context()).WithError(err).Error("InventoryHandler::CreateInventory -> request binding failed") validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::CreateInventory") return } validationError, validationErrorCode := h.inventoryValidator.ValidateCreateInventoryRequest(&req) if validationError != nil { validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::CreateInventory") return } inventoryResponse := h.inventoryService.CreateInventory(ctx, contextInfo, &req) if inventoryResponse.HasErrors() { errorResp := inventoryResponse.GetErrors()[0] logger.FromContext(ctx).WithError(errorResp).Error("InventoryHandler::CreateInventory -> Failed to create inventory from service") } util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::CreateInventory") } func (h *InventoryHandler) UpdateInventory(c *gin.Context) { ctx := c.Request.Context() inventoryIDStr := c.Param("id") inventoryID, err := uuid.Parse(inventoryIDStr) if err != nil { logger.FromContext(ctx).WithError(err).Error("InventoryHandler::UpdateInventory -> Invalid inventory ID") validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid inventory ID") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::UpdateInventory") return } var req contract.UpdateInventoryRequest if err := c.ShouldBindJSON(&req); err != nil { logger.FromContext(ctx).WithError(err).Error("InventoryHandler::UpdateInventory -> request binding failed") validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, "Invalid request body") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::UpdateInventory") return } validationError, validationErrorCode := h.inventoryValidator.ValidateUpdateInventoryRequest(&req) if validationError != nil { validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::UpdateInventory") return } inventoryResponse := h.inventoryService.UpdateInventory(ctx, inventoryID, &req) if inventoryResponse.HasErrors() { errorResp := inventoryResponse.GetErrors()[0] logger.FromContext(ctx).WithError(errorResp).Error("InventoryHandler::UpdateInventory -> Failed to update inventory from service") } util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::UpdateInventory") } func (h *InventoryHandler) DeleteInventory(c *gin.Context) { ctx := c.Request.Context() inventoryIDStr := c.Param("id") inventoryID, err := uuid.Parse(inventoryIDStr) if err != nil { logger.FromContext(ctx).WithError(err).Error("InventoryHandler::DeleteInventory -> Invalid inventory ID") validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid inventory ID") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::DeleteInventory") return } inventoryResponse := h.inventoryService.DeleteInventory(ctx, inventoryID) if inventoryResponse.HasErrors() { errorResp := inventoryResponse.GetErrors()[0] logger.FromContext(ctx).WithError(errorResp).Error("InventoryHandler::DeleteInventory -> Failed to delete inventory from service") } util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::DeleteInventory") } func (h *InventoryHandler) GetInventory(c *gin.Context) { ctx := c.Request.Context() inventoryIDStr := c.Param("id") inventoryID, err := uuid.Parse(inventoryIDStr) if err != nil { logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetInventory -> Invalid inventory ID") validationResponseError := contract.NewResponseError(constants.MalformedFieldErrorCode, constants.RequestEntity, "Invalid inventory ID") util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::GetInventory") return } inventoryResponse := h.inventoryService.GetInventoryByID(ctx, inventoryID) if inventoryResponse.HasErrors() { errorResp := inventoryResponse.GetErrors()[0] logger.FromContext(ctx).WithError(errorResp).Error("InventoryHandler::GetInventory -> Failed to get inventory from service") } util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::GetInventory") } func (h *InventoryHandler) ListInventory(c *gin.Context) { ctx := c.Request.Context() contextInfo := appcontext.FromGinContext(ctx) req := &contract.ListInventoryRequest{ Page: 1, Limit: 10, OutletID: &contextInfo.OutletID, } if pageStr := c.Query("page"); pageStr != "" { if page, err := strconv.Atoi(pageStr); err == nil { req.Page = page } } if limitStr := c.Query("limit"); limitStr != "" { if limit, err := strconv.Atoi(limitStr); err == nil { req.Limit = limit } } if search := c.Query("search"); search != "" { req.Search = search } if outletIDStr := c.Query("outlet_id"); outletIDStr != "" { if outletID, err := uuid.Parse(outletIDStr); err == nil { req.OutletID = &outletID } } if productIDStr := c.Query("product_id"); productIDStr != "" { if productID, err := uuid.Parse(productIDStr); err == nil { req.ProductID = &productID } } if categoryIDStr := c.Query("category_id"); categoryIDStr != "" { if categoryID, err := uuid.Parse(categoryIDStr); err == nil { req.CategoryID = &categoryID } } if lowStockStr := c.Query("low_stock_only"); lowStockStr != "" { if lowStock, err := strconv.ParseBool(lowStockStr); err == nil { req.LowStockOnly = &lowStock } } if zeroStockStr := c.Query("zero_stock_only"); zeroStockStr != "" { if zeroStock, err := strconv.ParseBool(zeroStockStr); err == nil { req.ZeroStockOnly = &zeroStock } } validationError, validationErrorCode := h.inventoryValidator.ValidateListInventoryRequest(req) if validationError != nil { logger.FromContext(ctx).WithError(validationError).Error("InventoryHandler::ListInventory -> request validation failed") validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::ListInventory") return } inventoryResponse := h.inventoryService.ListInventory(ctx, req) if inventoryResponse.HasErrors() { errorResp := inventoryResponse.GetErrors()[0] logger.FromContext(ctx).WithError(errorResp).Error("InventoryHandler::ListInventory -> Failed to list inventory from service") } util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::ListInventory") } func (h *InventoryHandler) AdjustInventory(c *gin.Context) { ctx := c.Request.Context() var req contract.AdjustInventoryRequest if err := c.ShouldBindJSON(&req); err != nil { logger.FromContext(ctx).WithError(err).Error("InventoryHandler::AdjustInventory -> request binding failed") validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::AdjustInventory") return } validationError, validationErrorCode := h.inventoryValidator.ValidateAdjustInventoryRequest(&req) if validationError != nil { validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::AdjustInventory") return } inventoryResponse := h.inventoryService.AdjustInventory(ctx, &req) if inventoryResponse.HasErrors() { errorResp := inventoryResponse.GetErrors()[0] logger.FromContext(ctx).WithError(errorResp).Error("InventoryHandler::AdjustInventory -> Failed to adjust inventory from service") } util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::AdjustInventory") } func (h *InventoryHandler) RestockInventory(c *gin.Context) { ctx := c.Request.Context() var req contract.RestockInventoryRequest if err := c.ShouldBindJSON(&req); err != nil { logger.FromContext(c.Request.Context()).WithError(err).Error("InventoryHandler::RestockInventory -> request binding failed") validationResponseError := contract.NewResponseError(constants.MissingFieldErrorCode, constants.RequestEntity, err.Error()) util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::RestockInventory") return } // TODO: Add validation for restock request // validationError, validationErrorCode := h.inventoryValidator.ValidateRestockInventoryRequest(&req) // if validationError != nil { // validationResponseError := contract.NewResponseError(validationErrorCode, constants.RequestEntity, validationError.Error()) // util.HandleResponse(c.Writer, c.Request, contract.BuildErrorResponse([]*contract.ResponseError{validationResponseError}), "InventoryHandler::RestockInventory") // return // } inventoryResponse := h.inventoryService.RestockInventory(ctx, &req) if inventoryResponse.HasErrors() { errorResp := inventoryResponse.GetErrors()[0] logger.FromContext(ctx).WithError(errorResp).Error("InventoryHandler::RestockInventory -> Failed to restock inventory from service") } util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::RestockInventory") } func (h *InventoryHandler) GetLowStockItems(c *gin.Context) { ctx := c.Request.Context() outletIDStr := c.Param("outlet_id") outletID, err := uuid.Parse(outletIDStr) if err != nil { logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetLowStockItems -> 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::GetLowStockItems") return } inventoryResponse := h.inventoryService.GetLowStockItems(ctx, outletID) if inventoryResponse.HasErrors() { errorResp := inventoryResponse.GetErrors()[0] logger.FromContext(ctx).WithError(errorResp).Error("InventoryHandler::GetLowStockItems -> Failed to get low stock items from service") } util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::GetLowStockItems") } func (h *InventoryHandler) GetZeroStockItems(c *gin.Context) { ctx := c.Request.Context() outletIDStr := c.Param("outlet_id") outletID, err := uuid.Parse(outletIDStr) if err != nil { logger.FromContext(ctx).WithError(err).Error("InventoryHandler::GetZeroStockItems -> 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::GetZeroStockItems") return } inventoryResponse := h.inventoryService.GetZeroStockItems(ctx, outletID) if inventoryResponse.HasErrors() { errorResp := inventoryResponse.GetErrors()[0] logger.FromContext(ctx).WithError(errorResp).Error("InventoryHandler::GetZeroStockItems -> Failed to get zero stock items from service") } util.HandleResponse(c.Writer, c.Request, inventoryResponse, "InventoryHandler::GetZeroStockItems") } 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 } // Parse date range parameters for summary var dateFrom, dateTo *time.Time if dateFromStr := c.Query("date_from"); dateFromStr != "" { if parsedDateFrom, err := time.Parse("2006-01-02", dateFromStr); err == nil { dateFrom = &parsedDateFrom } } if dateToStr := c.Query("date_to"); dateToStr != "" { if parsedDateTo, err := time.Parse("2006-01-02", dateToStr); err == nil { dateTo = &parsedDateTo } } summary, err := h.inventoryService.GetInventoryReportSummary(ctx, outletID, contextInfo.OrganizationID, dateFrom, dateTo) 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") } func (h *InventoryHandler) GetInventoryReportDetails(c *gin.Context) { ctx := c.Request.Context() contextInfo := appcontext.FromGinContext(ctx) filter := &models.InventoryReportFilter{} if outletIDStr := c.Param("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 } 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 } } dateFromStr := c.Query("date_from") dateToStr := c.Query("date_to") if fromTime, toTime, err := util.ParseDateRangeToJakartaTime(dateFromStr, dateToStr); err == nil { if fromTime != nil { filter.DateFrom = fromTime } if toTime != nil { filter.DateTo = toTime } } 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") }