feat: implement component-item management with CRUD operations and status updates
This commit is contained in:
238
internal/repositories/component_item_repository.go
Normal file
238
internal/repositories/component_item_repository.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
"wm-backend/global"
|
||||
"wm-backend/internal/mapper"
|
||||
"wm-backend/internal/models"
|
||||
db "wm-backend/sqlc_gen"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func CreateComponentItem(ctx context.Context, queries *db.Queries, body models.ComponentItem) (models.ComponentItem, error) {
|
||||
result, err := queries.CreateComponentItem(ctx, *mapper.ToModelComponentItem(&body))
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to create component item")
|
||||
return models.ComponentItem{}, err
|
||||
}
|
||||
return *mapper.ToDomainComponentItem(result), nil
|
||||
}
|
||||
|
||||
func GetComponentItemByID(ctx context.Context, queries *db.Queries, id int64) (models.ComponentItem, error) {
|
||||
result, err := queries.GetComponentItemByID(ctx, id)
|
||||
if err != nil {
|
||||
return models.ComponentItem{}, err
|
||||
}
|
||||
return *mapper.ToDomainComponentItem(result), nil
|
||||
}
|
||||
|
||||
func ListComponentItems(ctx context.Context, queries *db.Queries) ([]models.ComponentItem, error) {
|
||||
results, err := queries.ListComponentItems(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var items []models.ComponentItem
|
||||
for _, r := range results {
|
||||
items = append(items, *mapper.ToDomainComponentItem(r))
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func UpdateComponentItem(ctx context.Context, queries *db.Queries, body models.ComponentItem) (models.ComponentItem, error) {
|
||||
result, err := queries.UpdateComponentItem(ctx, *mapper.ToUpdateModelComponentItem(&body))
|
||||
if err != nil {
|
||||
return models.ComponentItem{}, err
|
||||
}
|
||||
return *mapper.ToDomainComponentItem(result), nil
|
||||
}
|
||||
|
||||
func DeleteComponentItem(ctx context.Context, queries *db.Queries, id int64) (int64, error) {
|
||||
rowsAffected, err := queries.DeleteComponentItem(ctx, id)
|
||||
if err != nil {
|
||||
return rowsAffected, err
|
||||
}
|
||||
return rowsAffected, nil
|
||||
}
|
||||
|
||||
func FindComponentItems(ctx context.Context, queries *db.Queries, componentID int64) ([]models.FindComponentItemResult, error) {
|
||||
results, err := queries.FindComponentItem(ctx, componentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var items []models.FindComponentItemResult
|
||||
for _, r := range results {
|
||||
items = append(items, *mapper.ToDomainFindComponentItem(r))
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// UpdateComponentItemStatus changes the status of a component item within a transaction.
|
||||
// It handles three cases:
|
||||
// - Case 1: changedQuantity is nil or >= quantity → change status of entire record
|
||||
// - Case 2: changedQuantity < quantity, no existing record with target status → split into 2 records
|
||||
// - Case 3: changedQuantity < quantity, existing record with target status → merge quantities
|
||||
//
|
||||
// Returns UpdateStatusResult with case-specific fields populated.
|
||||
func UpdateComponentItemStatus(ctx context.Context, dbPool *pgxpool.Pool, id int64, newStatus db.ComponentItemStatusEnum,
|
||||
changedQuantity *int32,
|
||||
note string,
|
||||
changedBy string) (result models.UpdateStatusResult, err error) {
|
||||
|
||||
// 1. Begin transaction
|
||||
tx, err := dbPool.Begin(ctx)
|
||||
if err != nil {
|
||||
return models.UpdateStatusResult{}, fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
tx.Rollback(ctx)
|
||||
}
|
||||
}()
|
||||
|
||||
txQueries := global.Queries.WithTx(tx)
|
||||
|
||||
// 2. Get existing item
|
||||
existingItem, err := txQueries.GetComponentItemByID(ctx, id)
|
||||
if err != nil {
|
||||
return models.UpdateStatusResult{}, fmt.Errorf("item not found: %w", err)
|
||||
}
|
||||
|
||||
// 3. Check status unchanged
|
||||
if existingItem.Status == newStatus {
|
||||
tx.Rollback(ctx)
|
||||
return models.UpdateStatusResult{}, fmt.Errorf("status unchanged")
|
||||
}
|
||||
|
||||
// 4. Determine changed quantity (nil = change all)
|
||||
var changedQty int32
|
||||
if changedQuantity == nil {
|
||||
changedQty = existingItem.Quantity
|
||||
} else {
|
||||
changedQty = *changedQuantity
|
||||
}
|
||||
|
||||
// 5. Validate changed quantity
|
||||
if changedQty <= 0 {
|
||||
return models.UpdateStatusResult{}, fmt.Errorf("changed_quantity must be positive, got %d", changedQty)
|
||||
}
|
||||
if changedQty > existingItem.Quantity {
|
||||
return models.UpdateStatusResult{}, fmt.Errorf("changed_quantity (%d) exceeds current quantity (%d)", changedQty, existingItem.Quantity)
|
||||
}
|
||||
|
||||
// 6. Branch by case
|
||||
if changedQty >= existingItem.Quantity {
|
||||
// ── Case 1: Change status of entire record ──
|
||||
updatedItem, err := txQueries.UpdateComponentItemStatus(ctx, db.UpdateComponentItemStatusParams{
|
||||
ID: id,
|
||||
Status: newStatus,
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return models.UpdateStatusResult{}, fmt.Errorf("update status: %w", err)
|
||||
}
|
||||
result.ComponentItem = *mapper.ToDomainComponentItem(updatedItem)
|
||||
} else {
|
||||
// ── Case 2 & 3: Partial change ──
|
||||
|
||||
// 6a. Reduce quantity on original record (or delete if qty becomes 0)
|
||||
newQty := existingItem.Quantity - changedQty
|
||||
if newQty == 0 {
|
||||
_, err = txQueries.DeleteComponentItem(ctx, id)
|
||||
if err != nil {
|
||||
return models.UpdateStatusResult{}, fmt.Errorf("delete zero-qty record: %w", err)
|
||||
}
|
||||
} else {
|
||||
_, err = txQueries.UpdateComponentItemQuantity(ctx, db.UpdateComponentItemQuantityParams{
|
||||
ID: id,
|
||||
Quantity: newQty,
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return models.UpdateStatusResult{}, fmt.Errorf("update quantity: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 6b. Find existing record with same (component_id, container_id, new_status)
|
||||
existingNewStatus, findErr := txQueries.GetComponentItemByComponentContainerStatus(ctx,
|
||||
db.GetComponentItemByComponentContainerStatusParams{
|
||||
ComponentID: existingItem.ComponentID,
|
||||
ContainerID: existingItem.ContainerID,
|
||||
Status: newStatus,
|
||||
})
|
||||
|
||||
if findErr != nil {
|
||||
if errors.Is(findErr, pgx.ErrNoRows) {
|
||||
// ── Case 2: No existing record → create new one ──
|
||||
newItem, createErr := txQueries.CreateComponentItem(ctx, db.CreateComponentItemParams{
|
||||
ComponentID: existingItem.ComponentID,
|
||||
ContainerID: existingItem.ContainerID,
|
||||
Quantity: changedQty,
|
||||
Status: newStatus,
|
||||
Metadata: existingItem.Metadata,
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
if createErr != nil {
|
||||
return models.UpdateStatusResult{}, fmt.Errorf("create new item: %w", createErr)
|
||||
}
|
||||
newID := newItem.ID
|
||||
result.NewComponentItemID = &newID
|
||||
} else {
|
||||
return models.UpdateStatusResult{}, fmt.Errorf("find existing new status: %w", findErr)
|
||||
}
|
||||
} else {
|
||||
// ── Case 3: Existing record → merge quantities ──
|
||||
mergedQty := existingNewStatus.Quantity + changedQty
|
||||
_, err = txQueries.UpdateComponentItemQuantity(ctx, db.UpdateComponentItemQuantityParams{
|
||||
ID: existingNewStatus.ID,
|
||||
Quantity: mergedQty,
|
||||
UpdatedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return models.UpdateStatusResult{}, fmt.Errorf("merge quantity: %w", err)
|
||||
}
|
||||
mergedID := existingNewStatus.ID
|
||||
result.MergedComponentItemID = &mergedID
|
||||
}
|
||||
}
|
||||
|
||||
// 7. Insert status history record
|
||||
history, err := txQueries.CreateComponentStatusHistory(ctx, db.CreateComponentStatusHistoryParams{
|
||||
ComponentItemID: id,
|
||||
OldStatus: db.NullComponentItemStatusEnum{
|
||||
ComponentItemStatusEnum: existingItem.Status,
|
||||
Valid: true,
|
||||
},
|
||||
NewStatus: newStatus,
|
||||
ChangedQuantity: pgtype.Int4{
|
||||
Int32: changedQty,
|
||||
Valid: true,
|
||||
},
|
||||
Note: pgtype.Text{
|
||||
String: note,
|
||||
Valid: note != "",
|
||||
},
|
||||
ChangedBy: pgtype.Text{
|
||||
String: changedBy,
|
||||
Valid: true,
|
||||
},
|
||||
ChangedAt: time.Now(),
|
||||
})
|
||||
if err != nil {
|
||||
return models.UpdateStatusResult{}, fmt.Errorf("insert history: %w", err)
|
||||
}
|
||||
result.StatusHistory = *mapper.ToDomainComponentStatusHistory(history)
|
||||
|
||||
// 8. Commit transaction
|
||||
if err = tx.Commit(ctx); err != nil {
|
||||
return models.UpdateStatusResult{}, fmt.Errorf("commit: %w", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
Reference in New Issue
Block a user