api-v1/routes/listingRoutes.js

627 lines
16 KiB
JavaScript

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;