import express from "express"; import pool from "../db/pool.js"; const router = express.Router(); const createNewLocation = async (client, userId, locationData) => { const { lat, lng, source_type, source_confidence, selected_location, // Location Details is_saved_address, location_type, country, state, district, city_village, pincode, } = locationData; // 1. Insert into locations (Merged Table) const insertLocationQuery = ` INSERT INTO locations ( user_id, lat, lng, source_type, source_confidence, selected_location, is_saved_address, location_type, country, state, district, city_village, pincode ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id `; const locationValues = [ userId, lat, lng, source_type || "manual", source_confidence || "unknown", selected_location || false, is_saved_address || false, location_type || "other", country, state, district, city_village, pincode ]; try { const locationResult = await client.query(insertLocationQuery, locationValues); if (locationResult.rows.length === 0) { throw new Error("Failed to insert location record"); } return locationResult.rows[0].id; } catch (error) { console.error("Error creating new location:", error); throw error; // Propagate error to trigger transaction rollback } }; // 1. GET / (Main Feed) - Optimized with idx_listings_feed_optimized router.get("/", async (req, res) => { try { const { status = "active", species_id, price_min, price_max, limit = 20, offset = 0 } = req.query; let queryText = ` SELECT * FROM listings WHERE deleted = FALSE AND status = $1 `; const queryParams = [status]; let paramCount = 1; if (species_id) { paramCount++; queryText += ` AND filter_species_id = $${paramCount}`; queryParams.push(species_id); } // New Filters if (req.query.breed_id) { paramCount++; queryText += ` AND filter_breed_id = $${paramCount}`; queryParams.push(req.query.breed_id); } if (req.query.sex) { paramCount++; queryText += ` AND filter_sex = $${paramCount}`; queryParams.push(req.query.sex); } if (req.query.pregnancy_status) { paramCount++; queryText += ` AND filter_pregnancy_status = $${paramCount}`; queryParams.push(req.query.pregnancy_status); } // Range Filters if (req.query.age_min) { paramCount++; queryText += ` AND filter_age_months >= $${paramCount}`; queryParams.push(req.query.age_min); } if (req.query.age_max) { paramCount++; queryText += ` AND filter_age_months <= $${paramCount}`; queryParams.push(req.query.age_max); } if (req.query.weight_min) { paramCount++; queryText += ` AND filter_weight_kg >= $${paramCount}`; queryParams.push(req.query.weight_min); } if (req.query.weight_max) { paramCount++; queryText += ` AND filter_weight_kg <= $${paramCount}`; queryParams.push(req.query.weight_max); } if (req.query.calving_number_min) { paramCount++; queryText += ` AND filter_calving_number >= $${paramCount}`; queryParams.push(req.query.calving_number_min); } if (req.query.calving_number_max) { paramCount++; queryText += ` AND filter_calving_number <= $${paramCount}`; queryParams.push(req.query.calving_number_max); } if (req.query.milking_capacity_min) { paramCount++; queryText += ` AND filter_milking_capacity >= $${paramCount}`; queryParams.push(req.query.milking_capacity_min); } if (req.query.milking_capacity_max) { paramCount++; queryText += ` AND filter_milking_capacity <= $${paramCount}`; queryParams.push(req.query.milking_capacity_max); } if (price_min) { paramCount++; queryText += ` AND price >= $${paramCount}`; queryParams.push(price_min); } if (price_max) { paramCount++; queryText += ` AND price <= $${paramCount}`; queryParams.push(price_max); } queryText += ` ORDER BY created_at DESC LIMIT $${paramCount + 1} OFFSET $${paramCount + 2}`; queryParams.push(limit, offset); const result = await pool.query(queryText, queryParams); res.json(result.rows); } catch (err) { console.error("Error fetching listings feed:", err); res.status(500).json({ error: "Internal server error" }); } }); // 2. GET /near-me (Spatial Search) - Optimized with idx_listings_spatial router.get("/near-me", async (req, res) => { try { const { lat, lng, radius_meters = 50000, limit = 20, offset = 0 } = req.query; if (!lat || !lng) { return res.status(400).json({ error: "Latitude and Longitude are required" }); } const queryText = ` SELECT * FROM listings WHERE deleted = FALSE AND ST_DWithin(filter_location_geog, ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography, $3) ORDER BY filter_location_geog <-> ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography LIMIT $4 OFFSET $5 `; const queryParams = [lng, lat, radius_meters, limit, offset]; // Note: PostGIS uses (lng, lat) for Points const result = await pool.query(queryText, queryParams); res.json(result.rows); } catch (err) { console.error("Error fetching nearby listings:", err); res.status(500).json({ error: "Internal server error" }); } }); // 3. GET /search TODO: (Full Text Search) - Optimized with idx_listings_search_gin router.get("/search", async (req, res) => { try { const { q, limit = 20, offset = 0 } = req.query; if (!q) { return res.status(400).json({ error: "Search query 'q' is required" }); } const queryText = ` SELECT * FROM listings WHERE deleted = FALSE AND search_vector @@ plainto_tsquery('english', $1) LIMIT $2 OFFSET $3 `; const queryParams = [q, limit, offset]; const result = await pool.query(queryText, queryParams); res.json(result.rows); } catch (err) { console.error("Error searching listings:", err); res.status(500).json({ error: "Internal server error" }); } }); // 4. GET /seller/:sellerId (My Listings) - Optimized with idx_listings_seller_status router.get("/seller/:sellerId", async (req, res) => { try { const { sellerId } = req.params; const { status } = req.query; let queryText = ` SELECT * FROM listings WHERE deleted = FALSE AND seller_id = $1 `; const queryParams = [sellerId]; let paramCount = 1; if (status) { paramCount++; queryText += ` AND status = $${paramCount}`; queryParams.push(status); } queryText += ` ORDER BY created_at DESC`; const result = await pool.query(queryText, queryParams); res.json(result.rows); } catch (err) { console.error("Error fetching seller listings:", err); res.status(500).json({ error: "Internal server error" }); } }); // 5. CRUD Operations // CREATE Listing router.post("/", async (req, res) => { const client = await pool.connect(); try { await client.query("BEGIN"); const { // Listing details seller_id, title, price, currency, is_negotiable, listing_type, animal, media // Array of { media_url, media_type, is_primary, sort_order } } = req.body; let final_location_id = animal?.location_id; // 1. Create Location (if needed) if (!final_location_id && animal?.new_location) { final_location_id = await createNewLocation(client, seller_id, animal.new_location); } // 2. Create Animal const insertAnimalQuery = ` INSERT INTO animals ( species_id, breed_id, location_id, sex, age_months, weight_kg, color_markings, quantity, purpose, health_status, vaccinated, dewormed, pregnancy_status, calving_number, milk_yield_litre_per_day, ear_tag_no, description ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17) RETURNING id `; const animalValues = [ animal.species_id, animal.breed_id, final_location_id, animal.sex, animal.age_months, animal.weight_kg, animal.color_markings, animal.quantity || 1, // Default to 1 animal.purpose, animal.health_status || "healthy", // Default animal.vaccinated || false, animal.dewormed || false, animal.pregnancy_status || "unknown", // Default animal.calving_number, // Added animal.milk_yield_litre_per_day, animal.ear_tag_no, animal.description, ]; const animalResult = await client.query(insertAnimalQuery, animalValues); const animal_id = animalResult.rows[0].id; // 3. Create Listing const insertListingQuery = ` INSERT INTO listings (seller_id, animal_id, title, price, currency, is_negotiable, listing_type) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING * `; const listingValues = [ seller_id, animal_id, title, price, currency, is_negotiable, listing_type ]; const listingResult = await client.query(insertListingQuery, listingValues); const listing_id = listingResult.rows[0].id; // 4. Create Listing Media if (media && media.length > 0) { const mediaInsertQuery = ` INSERT INTO listing_media (listing_id, media_url, media_type, is_primary, sort_order) VALUES ($1, $2, $3, $4, $5) `; for (const item of media) { await client.query(mediaInsertQuery, [ listing_id, item.media_url, item.media_type, // 'image', 'video' item.is_primary || false, item.sort_order || 0 ]); } } await client.query("COMMIT"); res.status(201).json(listingResult.rows[0]); } catch (err) { await client.query("ROLLBACK"); console.error("Error creating listing with animal:", err); res.status(500).json({ error: "Internal server error" }); } finally { client.release(); } }); // GET Single Listing router.get("/:id", async (req, res) => { try { const { id } = req.params; const queryText = ` SELECT l.*, row_to_json(a) as animal FROM listings l JOIN animals a ON l.animal_id = a.id WHERE l.id = $1 AND l.deleted = FALSE `; const result = await pool.query(queryText, [id]); if (result.rows.length === 0) { return res.status(404).json({ error: "Listing not found" }); } res.json(result.rows[0]); } catch (err) { console.error("Error fetching listing:", err); res.status(500).json({ error: "Internal server error" }); } }); // UPDATE Listing router.put("/:id", async (req, res) => { try { const { id } = req.params; const { title, price, currency, is_negotiable, listing_type, status } = req.body; const queryText = ` UPDATE listings SET title = COALESCE($1, title), price = COALESCE($2, price), currency = COALESCE($3, currency), is_negotiable = COALESCE($4, is_negotiable), listing_type = COALESCE($5, listing_type), status = COALESCE($6, status) WHERE id = $7 AND deleted = FALSE RETURNING * `; const queryParams = [title, price, currency, is_negotiable, listing_type, status, id]; const result = await pool.query(queryText, queryParams); if (result.rows.length === 0) { return res.status(404).json({ error: "Listing not found" }); } res.json(result.rows[0]); } catch (err) { console.error("Error updating listing:", err); res.status(500).json({ error: "Internal server error" }); } }); // SOFT DELETE Listing router.delete("/:id", async (req, res) => { try { const { id } = req.params; const { deleted_reason } = req.body; const queryText = ` UPDATE listings SET deleted = TRUE, deleted_reason = $1 WHERE id = $2 RETURNING * `; const result = await pool.query(queryText, [deleted_reason, id]); if (result.rows.length === 0) { return res.status(404).json({ error: "Listing not found" }); } res.json({ message: "Listing deleted successfully", listing: result.rows[0] }); } catch (err) { console.error("Error deleting listing:", err); res.status(500).json({ error: "Internal server error" }); } }); // Get the list of breeds router.get("/breeds", async (req, res) => { try { const queryText = "SELECT * FROM breeds"; const result = await pool.query(queryText); res.json(result.rows); } catch (err) { console.error("Error fetching breeds:", err); res.status(500).json({ error: "Internal server error" }); } }); // Get user favorite listings router.get("/user/:userId/favorites", async (req, res) => { try { const { userId } = req.params; const queryText = ` SELECT l.*, row_to_json(a) as animal FROM favorites f JOIN listings l ON f.listing_id = l.id JOIN animals a ON l.animal_id = a.id WHERE f.user_id = $1 AND f.deleted = FALSE AND l.deleted = FALSE ORDER BY f.created_at DESC `; const result = await pool.query(queryText, [userId]); res.json(result.rows); } catch (err) { console.error("Error fetching favorite listings:", err); res.status(500).json({ error: "Internal server error" }); } }); // Get listings created by user router.get("/user/:userId", async (req, res) => { try { const { userId } = req.params; const queryText = ` SELECT l.*, row_to_json(a) as animal FROM listings l JOIN animals a ON l.animal_id = a.id WHERE l.deleted = FALSE AND l.seller_id = $1 ORDER BY l.created_at DESC `; const result = await pool.query(queryText, [userId]); res.json(result.rows); } catch (err) { console.error("Error fetching user listings:", err); res.status(500).json({ error: "Internal server error" }); } }); // Update listing score & status router.patch("/:id/score", async (req, res) => { try { const { id } = req.params; const { listing_score, listing_score_status } = req.body; const queryText = ` UPDATE listings SET listing_score = COALESCE($1, listing_score), listing_score_status = COALESCE($2, listing_score_status) WHERE id = $3 AND deleted = FALSE RETURNING * `; const queryParams = [listing_score, listing_score_status, id]; const result = await pool.query(queryText, queryParams); if (result.rows.length === 0) { return res.status(404).json({ error: "Listing not found" }); } res.json(result.rows[0]); } catch (err) { console.error("Error updating listing:", err); res.status(500).json({ error: "Internal server error" }); } }); export default router;