feat: implement component-item management with CRUD operations and status updates

This commit is contained in:
Tran Anh Tuan
2026-05-11 17:49:18 +07:00
parent 9ea72b4eea
commit 0ff65a18c0
23 changed files with 3870 additions and 0 deletions

View 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
}