package sync import ( "encoding/json" "fmt" "log/slog" "strings" "time" ) // SeenPartnumber represents an unresolved vendor partnumber to report. type SeenPartnumber struct { Partnumber string Description string Ignored bool LotSuggestion []LotSuggestionEntry // optional; set when user manually mapped PN → LOT in UI } // LotSuggestionEntry is one suggested LOT mapping for a vendor partnumber. // JSON shape mirrors qt_partnumber_book_items.lots_json: {"lot_name", "qty"}. type LotSuggestionEntry struct { LotName string `json:"lot_name"` Qty int `json:"qty"` } // PushPartnumberSeen inserts unresolved vendor partnumbers into qt_vendor_partnumber_seen on MariaDB. // When LotSuggestion is provided the column is updated too; if the column does not exist yet // (migration pending) the write is retried without it and a warning is logged — the app never panics. func (s *Service) PushPartnumberSeen(items []SeenPartnumber) error { if len(items) == 0 { return nil } mariaDB, err := s.getDB() if err != nil { return fmt.Errorf("database not available: %w", err) } now := time.Now().UTC() for _, item := range items { if item.Partnumber == "" { continue } if len(item.LotSuggestion) > 0 { suggJSON, marshalErr := json.Marshal(item.LotSuggestion) if marshalErr != nil { slog.Error("partnumber_seen: failed to marshal lot_suggestion, skipping suggestion", "partnumber", item.Partnumber, "error", marshalErr) suggJSON = nil } if suggJSON != nil { err = mariaDB.Exec(` INSERT INTO qt_vendor_partnumber_seen (source_type, vendor, partnumber, description, is_ignored, last_seen_at, lot_suggestion) VALUES ('manual', '', ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE lot_suggestion = VALUES(lot_suggestion), last_seen_at = NOW(3) `, item.Partnumber, item.Description, item.Ignored, now, string(suggJSON)).Error if err == nil { continue } // Column not yet migrated — fall through to insert without lot_suggestion. if !isUnknownColumnError(err) { slog.Error("partnumber_seen: failed to upsert with lot_suggestion", "partnumber", item.Partnumber, "error", err) continue } slog.Warn("partnumber_seen: lot_suggestion column missing (migration pending), inserting without it", "partnumber", item.Partnumber) } } // Insert without lot_suggestion (baseline behaviour or fallback). err = mariaDB.Exec(` INSERT INTO qt_vendor_partnumber_seen (source_type, vendor, partnumber, description, is_ignored, last_seen_at) VALUES ('manual', '', ?, ?, ?, ?) ON DUPLICATE KEY UPDATE partnumber = partnumber `, item.Partnumber, item.Description, item.Ignored, now).Error if err != nil { slog.Error("failed to insert partnumber_seen", "partnumber", item.Partnumber, "error", err) } } slog.Info("partnumber_seen pushed to server", "count", len(items)) return nil } // isUnknownColumnError returns true when MariaDB reports that a column does not exist. func isUnknownColumnError(err error) bool { if err == nil { return false } msg := strings.ToLower(err.Error()) return strings.Contains(msg, "unknown column") || strings.Contains(msg, "1054") }