package routes import ( "bssapp-backend/auth" "bssapp-backend/queries" "context" "encoding/json" "errors" "log" "net/http" "strconv" "strings" "time" ) // GET /api/pricing/products func GetProductPricingListHandler(w http.ResponseWriter, r *http.Request) { started := time.Now() traceID := buildPricingTraceID(r) w.Header().Set("X-Trace-ID", traceID) claims, ok := auth.GetClaimsFromContext(r.Context()) if !ok || claims == nil { log.Printf("[ProductPricing] trace=%s unauthorized method=%s path=%s", traceID, r.Method, r.URL.Path) http.Error(w, "unauthorized", http.StatusUnauthorized) return } log.Printf("[ProductPricing] trace=%s start user=%s id=%d", traceID, claims.Username, claims.ID) ctx, cancel := context.WithTimeout(r.Context(), 180*time.Second) defer cancel() limit := 500 if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" { if parsed, err := strconv.Atoi(raw); err == nil && parsed > 0 && parsed <= 10000 { limit = parsed } } afterProductCode := strings.TrimSpace(r.URL.Query().Get("after_product_code")) rows, err := queries.GetProductPricingList(ctx, limit+1, afterProductCode) if err != nil { if isPricingTimeoutLike(err, ctx.Err()) { log.Printf( "[ProductPricing] trace=%s timeout user=%s id=%d duration_ms=%d err=%v", traceID, claims.Username, claims.ID, time.Since(started).Milliseconds(), err, ) http.Error(w, "Urun fiyatlandirma listesi zaman asimina ugradi", http.StatusGatewayTimeout) return } log.Printf( "[ProductPricing] trace=%s query_error user=%s id=%d duration_ms=%d err=%v", traceID, claims.Username, claims.ID, time.Since(started).Milliseconds(), err, ) http.Error(w, "Urun fiyatlandirma listesi alinamadi: "+err.Error(), http.StatusInternalServerError) return } hasMore := len(rows) > limit if hasMore { rows = rows[:limit] } nextCursor := "" if hasMore && len(rows) > 0 { nextCursor = strings.TrimSpace(rows[len(rows)-1].ProductCode) } log.Printf( "[ProductPricing] trace=%s success user=%s id=%d limit=%d after=%q count=%d has_more=%t next=%q duration_ms=%d", traceID, claims.Username, claims.ID, limit, afterProductCode, len(rows), hasMore, nextCursor, time.Since(started).Milliseconds(), ) w.Header().Set("Content-Type", "application/json; charset=utf-8") if hasMore { w.Header().Set("X-Has-More", "true") } else { w.Header().Set("X-Has-More", "false") } if nextCursor != "" { w.Header().Set("X-Next-Cursor", nextCursor) } _ = json.NewEncoder(w).Encode(rows) } func buildPricingTraceID(r *http.Request) string { if r != nil { if id := strings.TrimSpace(r.Header.Get("X-Request-ID")); id != "" { return id } if id := strings.TrimSpace(r.Header.Get("X-Correlation-ID")); id != "" { return id } } return "pricing-" + strconv.FormatInt(time.Now().UnixNano(), 36) } func isPricingTimeoutLike(err error, ctxErr error) bool { if errors.Is(err, context.DeadlineExceeded) || errors.Is(ctxErr, context.DeadlineExceeded) { return true } if err == nil { return false } e := strings.ToLower(err.Error()) return strings.Contains(e, "timeout") || strings.Contains(e, "i/o timeout") || strings.Contains(e, "wsarecv") || strings.Contains(e, "connection attempt failed") || strings.Contains(e, "no connection could be made") || strings.Contains(e, "failed to respond") }