diff --git a/db/pool.js b/db/pool.js new file mode 100644 index 0000000..dc3909c --- /dev/null +++ b/db/pool.js @@ -0,0 +1,21 @@ +import pg from "pg"; +import "dotenv/config"; + +const { Pool } = pg; + +const baseConfig = {}; + +baseConfig.host = process.env.PGHOST || "127.0.0.1"; +baseConfig.port = Number(process.env.PGPORT || 5432); +baseConfig.user = process.env.PGUSER || "postgres"; +baseConfig.password = process.env.PGPASSWORD || "postgres"; +baseConfig.database = process.env.PGDATABASE || "postgres"; + +console.log("Base Config:", baseConfig); +const pool = new Pool(baseConfig); + +pool.on("error", (err) => { + console.error("Unexpected Postgres client error", err); +}); + +export default pool; diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..f7cf5be --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,114 @@ +-- Livestock Marketplace schema (PostgreSQL) +-- Generated from specs in README.md + +CREATE EXTENSION IF NOT EXISTS "pgcrypto"; + +CREATE TABLE IF NOT EXISTS users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + name VARCHAR(255) NOT NULL, + phone VARCHAR(50) UNIQUE NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS species ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) UNIQUE NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS breeds ( + id SERIAL PRIMARY KEY, + species_id INT NOT NULL REFERENCES species(id) ON DELETE RESTRICT, + name VARCHAR(100) NOT NULL, + description TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS locations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID REFERENCES users(id) ON DELETE SET NULL, + is_saved_address BOOLEAN NOT NULL DEFAULT FALSE, + location_type VARCHAR(50), + country VARCHAR(100), + state VARCHAR(100), + district VARCHAR(100), + city_village VARCHAR(150), + pincode VARCHAR(20), + lat DECIMAL(10,7), + lng DECIMAL(10,7), + source_type VARCHAR(50) NOT NULL DEFAULT 'unknown', + source_confidence VARCHAR(50) NOT NULL DEFAULT 'medium', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS animals ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + species_id INT NOT NULL REFERENCES species(id) ON DELETE RESTRICT, + breed_id INT REFERENCES breeds(id) ON DELETE SET NULL, + sex VARCHAR(20) NOT NULL, + age_months INT, + weight_kg DECIMAL(10,2), + color_markings VARCHAR(200), + quantity INT NOT NULL DEFAULT 1, + purpose VARCHAR(50) NOT NULL, + health_status VARCHAR(50) NOT NULL, + vaccinated BOOLEAN NOT NULL DEFAULT FALSE, + dewormed BOOLEAN NOT NULL DEFAULT FALSE, + previous_pregnancies_count INT, + pregnancy_status VARCHAR(50), + milk_yield_litre_per_day DECIMAL(10,2), + ear_tag_no VARCHAR(100), + description TEXT, + suggested_care TEXT, + location_id UUID REFERENCES locations(id) ON DELETE SET NULL, + created_from VARCHAR(50) NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS listings ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + seller_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT, + animal_id UUID NOT NULL UNIQUE REFERENCES animals(id) ON DELETE CASCADE, + title VARCHAR(200) NOT NULL, + price NUMERIC(12,2) NOT NULL, + currency VARCHAR(10) NOT NULL, + is_negotiable BOOLEAN NOT NULL DEFAULT TRUE, + listing_type VARCHAR(50) NOT NULL, + status VARCHAR(50) NOT NULL DEFAULT 'active', + listing_score INT NOT NULL DEFAULT 0, + views_count INT NOT NULL DEFAULT 0, + bookmarks_count INT NOT NULL DEFAULT 0, + enquiries_call_count INT NOT NULL DEFAULT 0, + enquiries_whatsapp_count INT NOT NULL DEFAULT 0, + clicks_count INT NOT NULL DEFAULT 0, + listing_score_status VARCHAR(50) NOT NULL DEFAULT 'pending', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS listing_media ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + listing_id UUID NOT NULL REFERENCES listings(id) ON DELETE CASCADE, + media_url VARCHAR(500) NOT NULL, + media_type VARCHAR(50) NOT NULL, + is_primary BOOLEAN NOT NULL DEFAULT FALSE, + sort_order INT NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS custom_requirements ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE RESTRICT, + requirement_text TEXT NOT NULL, + animal_id UUID REFERENCES animals(id) ON DELETE SET NULL, + status VARCHAR(50) NOT NULL DEFAULT 'open', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + diff --git a/package-lock.json b/package-lock.json index 3bce4b4..f6287bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "cors": "^2.8.5", + "dotenv": "^17.2.3", "express": "^5.1.0", "pg": "^8.16.3" } @@ -156,6 +157,17 @@ "node": ">= 0.8" } }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/package.json b/package.json index ffdbf6e..9f9fdf7 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "homepage": "https://github.com/schari2509/BuySellService_LivingAI#readme", "dependencies": { "cors": "^2.8.5", + "dotenv": "^17.2.3", "express": "^5.1.0", "pg": "^8.16.3" } diff --git a/routes/listingRoutes.js b/routes/listingRoutes.js index aa67fd9..462a55e 100644 --- a/routes/listingRoutes.js +++ b/routes/listingRoutes.js @@ -1,14 +1,7 @@ import express from "express"; -const router = express.Router(); -import { Pool } from "pg"; +import pool from "../db/pool.js"; -const pool = new Pool({ - user: process.env.DB_USER, - host: process.env.DB_HOST, - database: process.env.DB_NAME, - password: process.env.DB_PASSWORD_D, - port: process.env.DB_PORT, -}); +const router = express.Router(); // Get all listings router.get("/", async (req, res) => { @@ -19,37 +12,43 @@ router.get("/", async (req, res) => { const minPrice = req.query.min_price; const listingType = req.query.listing_type; - let baseQuery = "SELECT * FROM listings WHERE 1=1"; + 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 species_id = $${paramIndex}`; + baseQuery += ` AND a.species_id = $${paramIndex}`; queryParams.push(speciesId); paramIndex++; } if (breedId) { - baseQuery += ` AND breed_id = $${paramIndex}`; + baseQuery += ` AND a.breed_id = $${paramIndex}`; queryParams.push(breedId); paramIndex++; } if (state) { - baseQuery += ` AND state = $${paramIndex}`; + baseQuery += ` AND loc.state = $${paramIndex}`; queryParams.push(state); paramIndex++; } if (district) { - baseQuery += ` AND district = $${paramIndex}`; + baseQuery += ` AND loc.district = $${paramIndex}`; queryParams.push(district); paramIndex++; } if (minPrice) { - baseQuery += ` AND price >= $${paramIndex}`; + baseQuery += ` AND l.price >= $${paramIndex}`; queryParams.push(minPrice); paramIndex++; } if (listingType) { - baseQuery += ` AND listing_type = $${paramIndex}`; + baseQuery += ` AND l.listing_type = $${paramIndex}`; queryParams.push(listingType); paramIndex++; } @@ -78,7 +77,7 @@ router.get("/:id", async (req, res) => { res.status(200).json(listingResult.rows[0]); } catch (error) { res.status(500).json({ - error: `Internal Server Error in fetching the specified ${id} listing`, + error: `Internal Server Error in fetching the specified ${listingId} listing`, }); } }); @@ -89,7 +88,7 @@ router.put("/:id", async (req, res) => { const { title, description, price } = req.body; try { const updateResult = await pool.query( - "UPDATE listings SET title = $1, description = $2, price = $3 WHERE id = $5 RETURNING *", + "UPDATE listings SET title = $1, description = $2, price = $3, updated_at = NOW() WHERE id = $4 RETURNING *", [title, description, price, listingId] ); if (updateResult.rows.length === 0) { @@ -100,7 +99,7 @@ router.put("/:id", async (req, res) => { res.status(200).json(updateResult.rows[0]); } catch (error) { res.status(500).json({ - error: `Internal Server Error in updating the specified ${id} listing`, + error: `Internal Server Error in updating the specified ${listingId} listing`, }); } }); @@ -123,10 +122,25 @@ router.post("/", async (req, res) => { // Add a new location if provided if (!animal.location_id && req.body.new_location) { - animal.location_id = addNewLocation(req.body.new_location); + const newLocation = { + ...req.body.new_location, + }; + if ( + newLocation.save_as_address && + !newLocation.user_id && + seller_id + ) { + newLocation.user_id = seller_id; + } + if (typeof newLocation.is_saved_address === "undefined") { + newLocation.is_saved_address = Boolean( + newLocation.save_as_address + ); + } + animal.location_id = await addNewLocation(client, newLocation); } - const animalId = await addAnimalToListing(animal); + 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 *"; @@ -156,10 +170,10 @@ router.post("/", async (req, res) => { } }); -const addAnimalToListing = async (animal) => { +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, NOW(), NOW()) RETURNING id"; + "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, @@ -179,6 +193,7 @@ const addAnimalToListing = async (animal) => { animal.description, animal.suggested_care, animal.location_id, + animal.created_from || "listing", ]; const animalResult = await client.query( animalInsertQuery, @@ -191,23 +206,38 @@ const addAnimalToListing = async (animal) => { } }; -const addNewLocation = async (location) => { +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 = [ - location.user_id, - location.is_saved_address, - location.location_type, - location.country, - location.state, - location.district, - location.city_village, - location.pincode, - location.lat, - location.lng, - location.source_type, - location.source_confidence, + 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, @@ -219,4 +249,72 @@ const addNewLocation = async (location) => { } }; +// 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", + }); + } +}); + +// 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; diff --git a/routes/locationRoutes.js b/routes/locationRoutes.js index 71f96c4..e2831c3 100644 --- a/routes/locationRoutes.js +++ b/routes/locationRoutes.js @@ -1,14 +1,7 @@ import express from "express"; -import { Pool } from "pg"; -const router = express.Router(); +import pool from "../db/pool.js"; -const pool = new Pool({ - user: process.env.DB_USER, - host: process.env.DB_HOST, - database: process.env.DB_NAME, - password: process.env.DB_PASSWORD_D, - port: process.env.DB_PORT, -}); +const router = express.Router(); // Add a new location router.post("/", async (req, res) => { @@ -112,6 +105,54 @@ router.put("/:id", async (req, res) => { } }); +// Get all locations for a user +router.get("/user/:user_id", async (req, res) => { + const userId = req.params.user_id; + try { + const result = await pool.query( + "SELECT * FROM locations WHERE user_id = $1 ORDER BY created_at DESC", + [userId] + ); + res.status(200).json(result.rows); + } catch (error) { + console.error("Error fetching user's locations:", error); + res.status(500).json({ + error: "Internal server error fetching user's locations", + }); + } +}); + +// TODO: Remove this route later +// Get all users +router.get("/users", async (req, res) => { + try { + const result = await pool.query( + "SELECT * FROM users ORDER BY created_at DESC" + ); + res.status(200).json(result.rows); + } catch (error) { + console.error("Error fetching all users:", error); + res.status(500).json({ + error: "Internal server error fetching users", + }); + } +}); + +// Get all locations +router.get("/", async (req, res) => { + try { + const result = await pool.query( + "SELECT * FROM locations ORDER BY created_at DESC" + ); + res.status(200).json(result.rows); + } catch (error) { + console.error("Error fetching all locations:", error); + res.status(500).json({ + error: "Internal server error fetching locations", + }); + } +}); + // Get location by ID router.get("/:id", async (req, res) => { const locationId = req.params.id; @@ -133,4 +174,27 @@ router.get("/:id", async (req, res) => { } }); +// Delete a location by ID +router.delete("/:id", async (req, res) => { + const locationId = req.params.id; + try { + const deleteQuery = "DELETE FROM locations WHERE id = $1 RETURNING *"; + const result = await pool.query(deleteQuery, [locationId]); + + if (result.rows.length === 0) { + return res.status(404).json({ error: "Location not found" }); + } + + res.status(200).json({ + message: "Location deleted successfully", + location: result.rows[0], + }); + } catch (error) { + console.error("Error deleting location:", error); + res.status(500).json({ + error: "Internal server error deleting location", + }); + } +}); + export default router;