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 }