import express from "express"; import pool from "../db/pool.js"; const router = express.Router(); // Get all listings router.get("/", async (req, res) => { const speciesId = req.query.species_id; const breedId = req.query.breed_id; const state = req.query.state; const district = req.query.district; const minPrice = req.query.min_price; const listingType = req.query.listing_type; let baseQuery = ` SELECT l.*, a.species_id, a.breed_id, loc.state, loc.district FROM listings l LEFT JOIN animals a ON a.id = l.animal_id LEFT JOIN locations loc ON loc.id = a.location_id WHERE 1=1 `; const queryParams = []; let paramIndex = 1; if (speciesId) { baseQuery += ` AND a.species_id = $${paramIndex}`; queryParams.push(speciesId); paramIndex++; } if (breedId) { baseQuery += ` AND a.breed_id = $${paramIndex}`; queryParams.push(breedId); paramIndex++; } if (state) { baseQuery += ` AND loc.state = $${paramIndex}`; queryParams.push(state); paramIndex++; } if (district) { baseQuery += ` AND loc.district = $${paramIndex}`; queryParams.push(district); paramIndex++; } if (minPrice) { baseQuery += ` AND l.price >= $${paramIndex}`; queryParams.push(minPrice); paramIndex++; } if (listingType) { baseQuery += ` AND l.listing_type = $${paramIndex}`; queryParams.push(listingType); paramIndex++; } try { const listingsResult = await pool.query(baseQuery, queryParams); res.status(200).json(listingsResult.rows); } catch (error) { res.status(500).json({ error: "Internal Server Error in fetching listings", }); } }); // Get listing by ID router.get("/:id", async (req, res) => { const listingId = req.params.id; try { const listingResult = await pool.query( "SELECT * FROM listings WHERE id = $1", [listingId] ); if (listingResult.rows.length === 0) { return res.status(404).json({ error: "Listing not found" }); } res.status(200).json(listingResult.rows[0]); } catch (error) { res.status(500).json({ error: `Internal Server Error in fetching the specified ${listingId} listing`, }); } }); // Update listing (and optionally its animal) by ID router.put("/:id", async (req, res) => { const listingId = req.params.id; const { title, price, currency, is_negotiable, listing_type } = req.body; const { animal } = req.body; if ( typeof title === "undefined" && typeof price === "undefined" && typeof currency === "undefined" && typeof is_negotiable === "undefined" && typeof listing_type === "undefined" && !animal ) { return res.status(400).json({ error: "Nothing to update. Provide at least one listing field or an animal object.", }); } const client = await pool.connect(); try { await client.query("BEGIN"); // Lock listing row and get its animal_id const listingResult = await client.query( "SELECT * FROM listings WHERE id = $1 FOR UPDATE", [listingId] ); if (listingResult.rows.length === 0) { await client.query("ROLLBACK"); return res.status(404).json({ error: "Listing not found" }); } const currentListing = listingResult.rows[0]; let updatedListing = currentListing; // Build dynamic listing UPDATE if any listing fields are provided const listingUpdates = []; const listingValues = []; let idx = 1; if (typeof title !== "undefined") { listingUpdates.push(`title = $${idx++}`); listingValues.push(title); } if (typeof price !== "undefined") { listingUpdates.push(`price = $${idx++}`); listingValues.push(price); } if (typeof currency !== "undefined") { listingUpdates.push(`currency = $${idx++}`); listingValues.push(currency); } if (typeof is_negotiable !== "undefined") { listingUpdates.push(`is_negotiable = $${idx++}`); listingValues.push(is_negotiable); } if (typeof listing_type !== "undefined") { listingUpdates.push(`listing_type = $${idx++}`); listingValues.push(listing_type); } if (listingUpdates.length > 0) { listingUpdates.push("updated_at = NOW()"); const listingUpdateQuery = ` UPDATE listings SET ${listingUpdates.join(", ")} WHERE id = $${idx} RETURNING *; `; listingValues.push(listingId); const updateResult = await client.query( listingUpdateQuery, listingValues ); updatedListing = updateResult.rows[0]; } let updatedAnimal = null; // Update the linked animal if payload is provided if (animal && updatedListing.animal_id) { const animalUpdates = []; const animalValues = []; let aIdx = 1; const updatableAnimalFields = [ "species_id", "breed_id", "sex", "age_months", "weight_kg", "color_markings", "quantity", "purpose", "health_status", "vaccinated", "dewormed", "previous_pregnancies_count", "pregnancy_status", "milk_yield_litre_per_day", "ear_tag_no", "description", "suggested_care", "location_id", ]; for (const field of updatableAnimalFields) { if (Object.prototype.hasOwnProperty.call(animal, field)) { animalUpdates.push(`${field} = $${aIdx++}`); animalValues.push(animal[field]); } } if (animalUpdates.length > 0) { animalUpdates.push("updated_at = NOW()"); const animalUpdateQuery = ` UPDATE animals SET ${animalUpdates.join(", ")} WHERE id = $${aIdx} RETURNING *; `; animalValues.push(updatedListing.animal_id); const animalResult = await client.query( animalUpdateQuery, animalValues ); if (animalResult.rows.length > 0) { updatedAnimal = animalResult.rows[0]; } } } await client.query("COMMIT"); return res.status(200).json({ message: "Listing (and animal, if provided) updated", listing: updatedListing, animal: updatedAnimal, }); } catch (error) { await client.query("ROLLBACK"); return res.status(500).json({ error: `Internal Server Error updating listing ${listingId}: ${error.message}`, }); } finally { client.release(); } }); // Submit a new listing router.post("/", async (req, res) => { const client = await pool.connect(); try { await client.query("BEGIN"); const { seller_id, title, price, currency, is_negotiable, listing_type, animal, } = req.body; // Add a new location if provided if (!animal.location_id && animal.new_location) { const newLocation = { ...animal.new_location, }; if ( newLocation.is_saved_address && !newLocation.user_id && seller_id ) { newLocation.user_id = seller_id; } animal.location_id = await addNewLocation(client, newLocation); } const animalId = await addAnimalToListing(client, animal); const listingInsertQuery = "INSERT INTO listings (seller_id, animal_id, title, price, currency, is_negotiable, listing_type, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW()) RETURNING *"; const listingValues = [ seller_id, animalId, title, price, currency, is_negotiable, listing_type, ]; const listingResult = await client.query( listingInsertQuery, listingValues ); if ( req.body.media && Array.isArray(req.body.media) && req.body.media.length > 0 ) { const listingId = listingResult.rows[0].id; const mediaInsertQuery = ` INSERT INTO listing_media (listing_id, media_url, media_type, is_primary, sort_order, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, NOW(), NOW()) RETURNING *; `; for (let i = 0; i < req.body.media.length; i++) { const media = req.body.media[i]; await client.query(mediaInsertQuery, [ listingId, media.media_url, media.media_type, typeof media.is_primary !== "undefined" ? media.is_primary : false, typeof media.sort_order !== "undefined" ? media.sort_order : i + 1, ]); } } await client.query("COMMIT"); res.status(201).json(listingResult.rows[0]); } catch (error) { await client.query("ROLLBACK"); res.status(500).json({ error: "Internal Server Error in creating new listing: " + error.message, }); } finally { client.release(); } }); const addAnimalToListing = async (client, animal) => { try { const animalInsertQuery = "INSERT INTO animals (species_id, breed_id, sex, age_months, weight_kg, color_markings, quantity, purpose, health_status, vaccinated, dewormed, previous_pregnancies_count, pregnancy_status, milk_yield_litre_per_day, ear_tag_no, description, suggested_care, location_id, created_from, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, NOW(), NOW()) RETURNING id"; const animalValues = [ animal.species_id, animal.breed_id, animal.sex, animal.age_months, animal.weight_kg, animal.color_markings, animal.quantity, animal.purpose, animal.health_status, animal.vaccinated, animal.dewormed, animal.previous_pregnancies_count, animal.pregnancy_status, animal.milk_yield_litre_per_day, animal.ear_tag_no, animal.description, animal.suggested_care, animal.location_id, animal.created_from || "listing", ]; const animalResult = await client.query( animalInsertQuery, animalValues ); const animalId = animalResult.rows[0].id; return animalId; } catch (error) { throw new Error("Error adding animal to listing: " + error.message); } }; const addNewLocation = async (client, location) => { try { const { user_id = null, is_saved_address = false, location_type = null, country = null, state = null, district = null, city_village = null, pincode = null, lat = null, lng = null, source_type = "unknown", source_confidence = "medium", } = location; const locationInsertQuery = "INSERT INTO locations (user_id, is_saved_address, location_type, country, state, district, city_village, pincode, lat, lng, source_type, source_confidence, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW(), NOW()) RETURNING id"; const locationValues = [ user_id, is_saved_address, location_type, country, state, district, city_village, pincode, lat, lng, source_type, source_confidence, ]; const locationResult = await client.query( locationInsertQuery, locationValues ); return locationResult.rows[0].id; } catch (error) { throw new Error("Error adding new location: " + error.message); } }; // Delete listing by ID router.delete("/:id", async (req, res) => { const listingId = req.params.id; try { const deleteResult = await pool.query( "DELETE FROM listings WHERE id = $1 RETURNING *", [listingId] ); if (deleteResult.rows.length === 0) { return res .status(404) .json({ error: "Listing not found or already deleted" }); } res.status(200).json({ message: "Listing deleted successfully", listing: deleteResult.rows[0], }); } catch (error) { res.status(500).json({ error: `Internal Server Error in deleting the specified ${listingId} listing`, }); } }); // Get all listings for a user router.get("/user/:user_id", async (req, res) => { const userId = req.params.user_id; try { const listingsResult = await pool.query( "SELECT * FROM listings WHERE seller_id = $1 ORDER BY created_at DESC", [userId] ); res.status(200).json(listingsResult.rows); } catch (error) { res.status(500).json({ error: "Internal Server Error in fetching user listings", }); } }); // Add media to listing router.post("/:id/media", async (req, res) => { const listingId = req.params.id; const { media } = req.body; if (!media || (Array.isArray(media) && media.length === 0)) { return res .status(400) .json({ error: "media array is required and cannot be empty" }); } const items = Array.isArray(media) ? media : [media]; try { const inserted = []; const insertSql = "INSERT INTO listing_media (listing_id, media_url, media_type, is_primary, sort_order, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, NOW(), NOW()) RETURNING *"; for (let i = 0; i < items.length; i++) { const item = items[i]; const result = await pool.query(insertSql, [ listingId, item.media_url, item.media_type, typeof item.is_primary !== "undefined" ? item.is_primary : false, typeof item.sort_order !== "undefined" ? item.sort_order : i + 1, ]); inserted.push(result.rows[0]); } res.status(200).json(inserted); } catch (error) { res.status(500).json({ error: `Internal Server Error adding media to listing ${listingId}: ${error.message}`, }); } }); // Update listing score and score status router.patch("/:id/score", async (req, res) => { const listingId = req.params.id; const { listing_score, listing_score_status } = req.body; if ( typeof listing_score === "undefined" && typeof listing_score_status === "undefined" ) { return res.status(400).json({ error: "At least one of listing_score or listing_score_status must be provided", }); } const updates = []; const values = []; let idx = 1; if (typeof listing_score !== "undefined") { updates.push(`listing_score = $${idx++}`); values.push(listing_score); } if (typeof listing_score_status !== "undefined") { updates.push(`listing_score_status = $${idx++}`); values.push(listing_score_status); } updates.push(`updated_at = NOW()`); const query = ` UPDATE listings SET ${updates.join(", ")} WHERE id = $${idx} RETURNING *; `; values.push(listingId); try { const result = await pool.query(query, values); if (result.rows.length === 0) { return res.status(404).json({ error: "Listing not found" }); } res.status(200).json({ message: "Listing listing_score and/or listing_score_status updated", listing: result.rows[0], }); } catch (error) { res.status(500).json({ error: `Internal Server Error updating listing_score/listing_score_status for listing ${listingId}: ${error.message}`, }); } }); // Update listing counters (views, bookmarks, enquiries, clicks) by incrementing router.patch("/:id/counter", async (req, res) => { const listingId = req.params.id; const { type, increment = 1 } = req.body; const validTypes = [ "views_count", "bookmarks_count", "enquiries_call_count", "enquiries_whatsapp_count", "clicks_count", ]; if (!type || !validTypes.includes(type)) { return res.status(400).json({ error: "Invalid or missing type. Must be one of: " + validTypes.join(", "), }); } if (typeof increment !== "number") { return res.status(400).json({ error: "Increment must be a number", }); } try { const updateResult = await pool.query( ` UPDATE listings SET ${type} = COALESCE(${type}, 0) + $1, updated_at = NOW() WHERE id = $2 RETURNING *; `, [increment, listingId] ); if (updateResult.rows.length === 0) { return res.status(404).json({ error: "Listing not found" }); } return res.status(200).json({ message: `Listing ${type} incremented by ${increment}`, listing: updateResult.rows[0], }); } catch (error) { res.status(500).json({ error: `Internal Server Error incrementing ${type} for listing ${listingId}: ${error.message}`, }); } }); // Mark listing as sold or update listing status router.patch("/:id/status", async (req, res) => { const listingId = req.params.id; const { status } = req.body; if (!status) { return res.status(400).json({ error: "Status must be provided" }); } try { const updateResult = await pool.query( "UPDATE listings SET status = $1, updated_at = NOW() WHERE id = $2 RETURNING *", [status, listingId] ); if (updateResult.rows.length === 0) { return res .status(404) .json({ error: "Listing not found for status update" }); } res.status(200).json({ message: "Listing status updated", listing: updateResult.rows[0], }); } catch (error) { res.status(500).json({ error: `Internal Server Error updating listing status for ${listingId}`, }); } }); export default router;