package repositories import ( "context" "fmt" "strings" "time" "github.com/CubeCraft-Creations/Extrudex/backend/internal/models" "github.com/jackc/pgx/v5/pgxpool" ) // FilamentRepository handles database queries for filament_spools. type FilamentRepository struct { pool *pgxpool.Pool } // NewFilamentRepository creates a FilamentRepository backed by the given pool. func NewFilamentRepository(pool *pgxpool.Pool) *FilamentRepository { return &FilamentRepository{pool: pool} } // FilamentFilter holds query parameters for listing filament spools. type FilamentFilter struct { Material string // filter by material_base name (case-insensitive) Finish string // filter by material_finish name (case-insensitive) Color string // filter by exact color_hex match LowStock bool // if true, filter for remaining_grams <= low_stock_threshold_grams Limit int Offset int } // spoolScanFields is the common SELECT column list for filament spools with JOINs. const spoolScanFields = ` s.id, s.name, s.material_base_id, COALESCE(mb.name, '') as material_base_name, COALESCE(mb.density_g_cm3, 0) as material_base_density_g_cm3, COALESCE(mb.extrusion_temp_min, NULL::int) as material_base_extrusion_temp_min, COALESCE(mb.extrusion_temp_max, NULL::int) as material_base_extrusion_temp_max, COALESCE(mb.bed_temp_min, NULL::int) as material_base_bed_temp_min, COALESCE(mb.bed_temp_max, NULL::int) as material_base_bed_temp_max, COALESCE(mb.created_at, s.created_at) as material_base_created_at, COALESCE(mb.updated_at, s.created_at) as material_base_updated_at, s.material_finish_id, COALESCE(mf.name, '') as material_finish_name, mf.description as material_finish_description, COALESCE(mf.created_at, s.created_at) as material_finish_created_at, COALESCE(mf.updated_at, s.created_at) as material_finish_updated_at, s.material_modifier_id, mm.name as material_modifier_name, mm.description as material_modifier_description, mm.created_at as material_modifier_created_at, mm.updated_at as material_modifier_updated_at, s.color_hex, s.brand, s.diameter_mm, s.initial_grams, s.remaining_grams, s.spool_weight_grams, s.cost_usd, s.low_stock_threshold_grams, s.notes, s.barcode, s.deleted_at, s.created_at, s.updated_at` const spoolFromJoins = ` FROM filament_spools s LEFT JOIN material_bases mb ON s.material_base_id = mb.id LEFT JOIN material_finishes mf ON s.material_finish_id = mf.id LEFT JOIN material_modifiers mm ON s.material_modifier_id = mm.id` // scanSpoolWithJoins scans a full spool row including all JOINed tables. func scanSpoolWithJoins(row interface{ Scan(...interface{}) error }) (models.FilamentSpool, error) { var s models.FilamentSpool var mb models.MaterialBase var mf models.MaterialFinish var mfDesc *string var modifierID *int var modName, modDesc *string var modCreatedAt, modUpdatedAt *time.Time err := row.Scan( &s.ID, &s.Name, &s.MaterialBaseID, &mb.Name, &mb.DensityGCm3, &mb.ExtrusionTempMin, &mb.ExtrusionTempMax, &mb.BedTempMin, &mb.BedTempMax, &mb.CreatedAt, &mb.UpdatedAt, &s.MaterialFinishID, &mf.Name, &mfDesc, &mf.CreatedAt, &mf.UpdatedAt, &modifierID, &modName, &modDesc, &modCreatedAt, &modUpdatedAt, &s.ColorHex, &s.Brand, &s.DiameterMM, &s.InitialGrams, &s.RemainingGrams, &s.SpoolWeightGrams, &s.CostUSD, &s.LowStockThresholdGrams, &s.Notes, &s.Barcode, &s.DeletedAt, &s.CreatedAt, &s.UpdatedAt, ) if err != nil { return s, err } mb.ID = s.MaterialBaseID s.MaterialBase = &mb mf.ID = s.MaterialFinishID if mfDesc != nil { mf.Description = mfDesc } s.MaterialFinish = &mf s.MaterialModifierID = modifierID if modifierID != nil && modName != nil { mm := models.MaterialModifier{ ID: *modifierID, Name: *modName, } if modDesc != nil { mm.Description = modDesc } if modCreatedAt != nil { mm.CreatedAt = *modCreatedAt } if modUpdatedAt != nil { mm.UpdatedAt = *modUpdatedAt } s.MaterialModifier = &mm } return s, nil } // GetAll returns filament spools matching the given filters, with pagination. // Returns results, total matching count, and any error. func (r *FilamentRepository) GetAll(ctx context.Context, filter FilamentFilter) ([]models.FilamentSpool, int, error) { conditions := []string{"s.deleted_at IS NULL"} args := []interface{}{} argIdx := 1 if filter.Material != "" { conditions = append(conditions, fmt.Sprintf("LOWER(mb.name) = LOWER($%d)", argIdx)) args = append(args, filter.Material) argIdx++ } if filter.Finish != "" { conditions = append(conditions, fmt.Sprintf("LOWER(mf.name) = LOWER($%d)", argIdx)) args = append(args, filter.Finish) argIdx++ } if filter.Color != "" { conditions = append(conditions, fmt.Sprintf("s.color_hex = $%d", argIdx)) args = append(args, filter.Color) argIdx++ } if filter.LowStock { conditions = append(conditions, "s.remaining_grams <= s.low_stock_threshold_grams") } whereClause := "" if len(conditions) > 0 { whereClause = "WHERE " + strings.Join(conditions, " AND ") } // Count total. var total int countQuery := "SELECT COUNT(*) " + spoolFromJoins + " " + whereClause if err := r.pool.QueryRow(ctx, countQuery, args...).Scan(&total); err != nil { return nil, 0, err } // Query with pagination. dataQuery := "SELECT " + spoolScanFields + " " + spoolFromJoins + " " + whereClause + " ORDER BY s.name ASC" + fmt.Sprintf(" LIMIT $%d OFFSET $%d", argIdx, argIdx+1) dataArgs := make([]interface{}, len(args)) copy(dataArgs, args) dataArgs = append(dataArgs, filter.Limit, filter.Offset) rows, err := r.pool.Query(ctx, dataQuery, dataArgs...) if err != nil { return nil, 0, err } defer rows.Close() var spools []models.FilamentSpool for rows.Next() { s, err := scanSpoolWithJoins(rows) if err != nil { return nil, 0, err } spools = append(spools, s) } if err := rows.Err(); err != nil { return nil, 0, err } if spools == nil { spools = []models.FilamentSpool{} } return spools, total, nil } // GetByID returns a single filament spool by ID with JOINed data. // Returns nil if not found or soft-deleted. func (r *FilamentRepository) GetByID(ctx context.Context, id int) (*models.FilamentSpool, error) { query := "SELECT " + spoolScanFields + " " + spoolFromJoins + " WHERE s.id = $1 AND s.deleted_at IS NULL" row := r.pool.QueryRow(ctx, query, id) s, err := scanSpoolWithJoins(row) if err != nil { return nil, err } return &s, nil } // Create inserts a new filament spool and returns the created spool with JOINed data. func (r *FilamentRepository) Create(ctx context.Context, spool *models.FilamentSpool) (*models.FilamentSpool, error) { var id int err := r.pool.QueryRow(ctx, ` INSERT INTO filament_spools ( name, material_base_id, material_finish_id, material_modifier_id, color_hex, brand, diameter_mm, initial_grams, remaining_grams, spool_weight_grams, cost_usd, low_stock_threshold_grams, notes, barcode ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14) RETURNING id `, spool.Name, spool.MaterialBaseID, spool.MaterialFinishID, spool.MaterialModifierID, spool.ColorHex, spool.Brand, spool.DiameterMM, spool.InitialGrams, spool.RemainingGrams, spool.SpoolWeightGrams, spool.CostUSD, spool.LowStockThresholdGrams, spool.Notes, spool.Barcode, ).Scan(&id) if err != nil { return nil, err } return r.GetByID(ctx, id) } // Update applies partial updates to an existing filament spool. // Only non-nil fields in the update map are applied. // Returns the updated spool. func (r *FilamentRepository) Update(ctx context.Context, id int, updates map[string]interface{}) (*models.FilamentSpool, error) { if len(updates) == 0 { return r.GetByID(ctx, id) } setClauses := []string{"updated_at = NOW()"} args := []interface{}{} argIdx := 1 for col, val := range updates { setClauses = append(setClauses, fmt.Sprintf("%s = $%d", col, argIdx)) args = append(args, val) argIdx++ } args = append(args, id) query := fmt.Sprintf("UPDATE filament_spools SET %s WHERE id = $%d AND deleted_at IS NULL", strings.Join(setClauses, ", "), argIdx) result, err := r.pool.Exec(ctx, query, args...) if err != nil { return nil, err } if result.RowsAffected() == 0 { return nil, nil // not found or deleted } return r.GetByID(ctx, id) } // SoftDelete marks a filament spool as deleted by setting deleted_at = NOW(). // Returns true if a row was affected. func (r *FilamentRepository) SoftDelete(ctx context.Context, id int) (bool, error) { result, err := r.pool.Exec(ctx, ` UPDATE filament_spools SET deleted_at = NOW(), updated_at = NOW() WHERE id = $1 AND deleted_at IS NULL `, id) if err != nil { return false, err } return result.RowsAffected() > 0, nil }