239 lines
7.5 KiB
Go
239 lines
7.5 KiB
Go
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
|
|
}
|