feat: implement component-item management with CRUD operations and status updates
This commit is contained in:
290
internal/services/component_item_service.go
Normal file
290
internal/services/component_item_service.go
Normal file
@@ -0,0 +1,290 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
"wm-backend/global"
|
||||
"wm-backend/internal/models"
|
||||
"wm-backend/internal/models/requests"
|
||||
"wm-backend/internal/models/responses"
|
||||
"wm-backend/internal/repositories"
|
||||
"wm-backend/pkg/helper"
|
||||
"wm-backend/response"
|
||||
db "wm-backend/sqlc_gen"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// ComponentItemCreate creates a new component item.
|
||||
// It validates the request body and creates the component item in the database.
|
||||
//
|
||||
// @Summary Create a new component item
|
||||
// @Description Create a new component item with the provided details
|
||||
// @Tags component-item
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param body body requests.CreateComponentItemRequest true "Component item request body"
|
||||
// @Success 201 {object} response.SuccessResponse{data=responses.CreateComponentItemResponse}
|
||||
// @Failure 400 {object} response.ErrorResponse
|
||||
// @Failure 500 {object} response.ErrorResponse
|
||||
// @Router /api/v1/component-items [post]
|
||||
func ComponentItemCreate(c *gin.Context) error {
|
||||
requestBody := requests.CreateComponentItemRequest{}
|
||||
if helper.IsShouldBindJSON(c, &requestBody) {
|
||||
return nil
|
||||
}
|
||||
componentItemModel := &models.ComponentItem{
|
||||
ComponentID: requestBody.ComponentID,
|
||||
ContainerID: requestBody.ContainerID,
|
||||
Quantity: requestBody.Quantity,
|
||||
Status: requestBody.Status,
|
||||
Metadata: requestBody.Metadata,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
componentItem, err := repositories.CreateComponentItem(c.Request.Context(), global.Queries, *componentItemModel)
|
||||
if err != nil {
|
||||
response.InternalServerError(c, http.StatusInternalServerError, "Failed to create component item")
|
||||
return nil
|
||||
}
|
||||
response.Created(c, "Component item created successfully", &responses.CreateComponentItemResponse{
|
||||
ID: componentItem.ID,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// ComponentItemGetByID retrieves a single component item by its ID.
|
||||
//
|
||||
// @Summary Get component item by ID
|
||||
// @Description Retrieve a single component item using its unique identifier
|
||||
// @Tags component-item
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Component item ID"
|
||||
// @Success 200 {object} response.SuccessResponse{data=models.ComponentItem}
|
||||
// @Failure 400 {object} response.ErrorResponse
|
||||
// @Failure 404 {object} response.ErrorResponse
|
||||
// @Failure 500 {object} response.ErrorResponse
|
||||
// @Router /api/v1/component-items/{id} [get]
|
||||
func ComponentItemGetByID(c *gin.Context) error {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequestError(c, http.StatusBadRequest, "Invalid ID")
|
||||
return nil
|
||||
}
|
||||
componentItem, err := repositories.GetComponentItemByID(c.Request.Context(), global.Queries, id)
|
||||
if err != nil {
|
||||
response.NotFoundError(c, http.StatusNotFound, "Component item not found")
|
||||
return nil
|
||||
}
|
||||
response.Ok(c, "Success", componentItem)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ComponentItemList retrieves all component items.
|
||||
//
|
||||
// @Summary List all component items
|
||||
// @Description Retrieve a list of all component items ordered by creation date
|
||||
// @Tags component-item
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} response.SuccessResponse{data=[]models.ComponentItem}
|
||||
// @Failure 500 {object} response.ErrorResponse
|
||||
// @Router /api/v1/component-items [get]
|
||||
func ComponentItemList(c *gin.Context) error {
|
||||
componentItems, err := repositories.ListComponentItems(c.Request.Context(), global.Queries)
|
||||
if err != nil {
|
||||
response.InternalServerError(c, http.StatusInternalServerError, "Failed to list component items")
|
||||
return nil
|
||||
}
|
||||
response.Ok(c, "Success", componentItems)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ComponentItemUpdate updates an existing component item by its ID.
|
||||
// It validates the request body, fetches the existing record,
|
||||
// merges non-empty fields from the request, and updates the component item in the database.
|
||||
//
|
||||
// @Summary Update component item
|
||||
// @Description Update an existing component item by its ID. Only non-empty fields will be updated.
|
||||
// @Tags component-item
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Component item ID"
|
||||
// @Param body body requests.UpdateComponentItemRequest true "Component item request body"
|
||||
// @Success 200 {object} response.SuccessResponse{data=responses.UpdateComponentItemResponse}
|
||||
// @Failure 400 {object} response.ErrorResponse
|
||||
// @Failure 404 {object} response.ErrorResponse
|
||||
// @Failure 500 {object} response.ErrorResponse
|
||||
// @Router /api/v1/component-items/{id} [put]
|
||||
func ComponentItemUpdate(c *gin.Context) error {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequestError(c, http.StatusBadRequest, "Invalid ID")
|
||||
return nil
|
||||
}
|
||||
requestBody := requests.UpdateComponentItemRequest{}
|
||||
if helper.IsShouldBindJSON(c, &requestBody) {
|
||||
return nil
|
||||
}
|
||||
existing, err := repositories.GetComponentItemByID(c.Request.Context(), global.Queries, id)
|
||||
if err != nil {
|
||||
response.NotFoundError(c, http.StatusNotFound, "Component item not found")
|
||||
return nil
|
||||
}
|
||||
if requestBody.ComponentID != nil {
|
||||
existing.ComponentID = *requestBody.ComponentID
|
||||
}
|
||||
if requestBody.ContainerID != nil {
|
||||
existing.ContainerID = *requestBody.ContainerID
|
||||
}
|
||||
if len(requestBody.Metadata) > 0 {
|
||||
existing.Metadata = requestBody.Metadata
|
||||
}
|
||||
existing.UpdatedAt = time.Now()
|
||||
componentItem, err := repositories.UpdateComponentItem(c.Request.Context(), global.Queries, existing)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Failed to update component item with ID: %d", id)
|
||||
response.InternalServerError(c, http.StatusInternalServerError, "Failed to update component item")
|
||||
return nil
|
||||
}
|
||||
response.Ok(c, "Component item updated successfully", &responses.UpdateComponentItemResponse{
|
||||
ID: componentItem.ID,
|
||||
ComponentID: componentItem.ComponentID,
|
||||
ContainerID: componentItem.ContainerID,
|
||||
Quantity: componentItem.Quantity,
|
||||
Status: componentItem.Status,
|
||||
Metadata: componentItem.Metadata,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// ComponentItemDelete deletes a component item by its ID.
|
||||
//
|
||||
// @Summary Delete component item
|
||||
// @Description Delete a component item by its unique identifier
|
||||
// @Tags component-item
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Component item ID"
|
||||
// @Success 200 {object} response.SuccessResponse
|
||||
// @Failure 400 {object} response.ErrorResponse
|
||||
// @Failure 500 {object} response.ErrorResponse
|
||||
// @Router /api/v1/component-items/{id} [delete]
|
||||
func ComponentItemDelete(c *gin.Context) error {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequestError(c, http.StatusBadRequest, "Invalid ID")
|
||||
return nil
|
||||
}
|
||||
rowsAffected, err := repositories.DeleteComponentItem(c.Request.Context(), global.Queries, id)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Failed to delete component item with ID: %d", id)
|
||||
response.InternalServerError(c, http.StatusInternalServerError, "Failed to delete component item")
|
||||
return nil
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
response.NotFoundError(c, http.StatusNotFound, "Component item not found")
|
||||
return nil
|
||||
}
|
||||
response.Ok(c, "Delete Success", nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ComponentItemFind retrieves component items by component ID with full location info.
|
||||
//
|
||||
// @Summary Find component items by component ID
|
||||
// @Description Retrieve component items with full location details (container, shelf, cabinet, room, warehouse) for a given component ID
|
||||
// @Tags component-item
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param componentId path int true "Component ID"
|
||||
// @Success 200 {object} response.SuccessResponse{data=[]models.FindComponentItemResult}
|
||||
// @Failure 400 {object} response.ErrorResponse
|
||||
// @Failure 500 {object} response.ErrorResponse
|
||||
// @Router /api/v1/component-items/find/{componentId} [get]
|
||||
func ComponentItemFind(c *gin.Context) error {
|
||||
componentID, err := strconv.ParseInt(c.Param("componentId"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequestError(c, http.StatusBadRequest, "Invalid component ID")
|
||||
return nil
|
||||
}
|
||||
items, err := repositories.FindComponentItems(c.Request.Context(), global.Queries, componentID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("Failed to find component items for component ID: %d", componentID)
|
||||
response.InternalServerError(c, http.StatusInternalServerError, "Failed to find component items")
|
||||
return nil
|
||||
}
|
||||
response.Ok(c, "Success", items)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ComponentItemUpdateStatus changes the status of a component item.
|
||||
// It handles three cases within a single transaction:
|
||||
// - Case 1 (change all): changedQuantity is nil or equals quantity → updates status of entire record
|
||||
// - Case 2 (split): changedQuantity < quantity, no existing record with target status → reduces old record, creates new record
|
||||
// - Case 3 (merge): changedQuantity < quantity, existing record with target status → reduces old record, merges into existing
|
||||
//
|
||||
// A history record is always created. If quantity becomes 0 after the split, the old record is deleted.
|
||||
//
|
||||
// @Summary Change component item status
|
||||
// @Description Change the status of a component item. Supports partial quantity change with automatic split/merge logic. A status history record is created automatically.
|
||||
// @Tags component-item
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "Component item ID"
|
||||
// @Param body body requests.UpdateComponentItemStatusRequest true "Status change request body"
|
||||
// @Success 200 {object} response.SuccessResponse{data=responses.UpdateComponentItemStatusResponse}
|
||||
// @Failure 400 {object} response.ErrorResponse "Validation error (e.g., changed_quantity > quantity, status unchanged)"
|
||||
// @Failure 404 {object} response.ErrorResponse "Component item not found"
|
||||
// @Failure 500 {object} response.ErrorResponse "Internal server error"
|
||||
// @Router /api/v1/component-items/{id}/status [put]
|
||||
func ComponentItemUpdateStatus(c *gin.Context) error {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.BadRequestError(c, http.StatusBadRequest, "Invalid ID")
|
||||
return nil
|
||||
}
|
||||
var req requests.UpdateComponentItemStatusRequest
|
||||
if helper.IsShouldBindJSON(c, &req) {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, err := repositories.UpdateComponentItemStatus(
|
||||
c.Request.Context(),
|
||||
global.DB,
|
||||
id,
|
||||
db.ComponentItemStatusEnum(req.Status),
|
||||
req.ChangedQuantity,
|
||||
req.Note,
|
||||
"system",
|
||||
)
|
||||
if err != nil {
|
||||
if err.Error() == "status unchanged" {
|
||||
response.Ok(c, "Status unchanged", nil)
|
||||
return nil
|
||||
}
|
||||
log.Error().Err(err).Msgf("Failed to update component item status for ID: %d", id)
|
||||
response.BadRequestError(c, http.StatusBadRequest, err.Error())
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build response based on case
|
||||
resp := responses.UpdateComponentItemStatusResponse{
|
||||
ID: id,
|
||||
OldStatus: result.StatusHistory.OldStatus,
|
||||
NewStatus: result.StatusHistory.NewStatus,
|
||||
ChangedQuantity: result.StatusHistory.ChangedQuantity,
|
||||
HistoryID: result.StatusHistory.ID,
|
||||
}
|
||||
if result.NewComponentItemID != nil {
|
||||
resp.NewComponentItemID = result.NewComponentItemID
|
||||
}
|
||||
if result.MergedComponentItemID != nil {
|
||||
resp.MergedComponentItemID = result.MergedComponentItemID
|
||||
}
|
||||
|
||||
response.Ok(c, "Status updated successfully", resp)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user