diff --git a/BuySellService_API.postman_collection.json b/BuySellService_API.postman_collection.json new file mode 100644 index 0000000..13fdb7b --- /dev/null +++ b/BuySellService_API.postman_collection.json @@ -0,0 +1,637 @@ +{ + "info": { + "_postman_id": "buysellservice-api-collection", + "name": "BuySellService API", + "description": "Complete API collection for Livestock Marketplace - BuySellService", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Users", + "item": [ + { + "name": "Create User", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"id\": \"aaf295cb-a19e-4179-a2df-31c0c64ea9f4\",\n \"name\": \"Test User\",\n \"phone_number\": \"+919876543210\",\n \"avatar_url\": null,\n \"language\": \"en\",\n \"timezone\": \"Asia/Kolkata\",\n \"country_code\": \"+91\"\n}" + }, + "url": { + "raw": "http://localhost:3200/users", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3200", + "path": [ + "users" + ] + }, + "description": "Create a new user. You can provide a specific UUID or let the system generate one." + } + }, + { + "name": "Get All Users", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3200/users?limit=100&offset=0", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3200", + "path": [ + "users" + ], + "query": [ + { + "key": "limit", + "value": "100" + }, + { + "key": "offset", + "value": "0" + } + ] + }, + "description": "Get a list of all users" + } + }, + { + "name": "Get User by ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3200/users/:userId", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3200", + "path": [ + "users", + ":userId" + ], + "variable": [ + { + "key": "userId", + "value": "aaf295cb-a19e-4179-a2df-31c0c64ea9f4" + } + ] + }, + "description": "Get a single user by UUID" + } + }, + { + "name": "Update User", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"Updated User Name\",\n \"phone_number\": \"+919876543210\"\n}" + }, + "url": { + "raw": "http://localhost:3200/users/:userId", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3200", + "path": [ + "users", + ":userId" + ], + "variable": [ + { + "key": "userId", + "value": "aaf295cb-a19e-4179-a2df-31c0c64ea9f4" + } + ] + }, + "description": "Update user information" + } + } + ] + }, + { + "name": "Listings", + "item": [ + { + "name": "Get All Listings", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3200/listings?status=active&limit=20&offset=0", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3200", + "path": [ + "listings" + ], + "query": [ + { + "key": "status", + "value": "active" + }, + { + "key": "limit", + "value": "20" + }, + { + "key": "offset", + "value": "0" + } + ] + }, + "description": "Get all active listings" + } + }, + { + "name": "Get Listing by ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3200/listings/:listingId", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3200", + "path": [ + "listings", + ":listingId" + ], + "variable": [ + { + "key": "listingId", + "value": "your-listing-uuid-here" + } + ] + }, + "description": "Get a single listing with full details" + } + }, + { + "name": "Create Listing", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"seller_id\": \"aaf295cb-a19e-4179-a2df-31c0c64ea9f4\",\n \"title\": \"High-yield Gir cow for sale\",\n \"price\": 55000,\n \"currency\": \"INR\",\n \"is_negotiable\": true,\n \"listing_type\": \"sale\",\n \"animal\": {\n \"species_id\": \"your-species-uuid\",\n \"breed_id\": \"your-breed-uuid\",\n \"sex\": \"F\",\n \"age_months\": 36,\n \"weight_kg\": 450,\n \"color_markings\": \"Brown with white patches\",\n \"quantity\": 1,\n \"purpose\": \"dairy\",\n \"health_status\": \"healthy\",\n \"vaccinated\": true,\n \"dewormed\": true,\n \"pregnancy_status\": \"pregnant\",\n \"milk_yield_litre_per_day\": 15,\n \"ear_tag_no\": \"TAG-12345\",\n \"description\": \"Calm nature, easy to handle.\"\n },\n \"media\": [\n {\n \"media_url\": \"https://cdn.app.com/listings/abc1.jpg\",\n \"media_type\": \"image\",\n \"is_primary\": true,\n \"sort_order\": 1\n }\n ]\n}" + }, + "url": { + "raw": "http://localhost:3200/listings", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3200", + "path": [ + "listings" + ] + }, + "description": "Create a new listing with animal details" + } + }, + { + "name": "Update Listing", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Updated title\",\n \"price\": 52000,\n \"status\": \"active\"\n}" + }, + "url": { + "raw": "http://localhost:3200/listings/:listingId", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3200", + "path": [ + "listings", + ":listingId" + ], + "variable": [ + { + "key": "listingId", + "value": "your-listing-uuid-here" + } + ] + }, + "description": "Update listing information" + } + }, + { + "name": "Search Listings", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3200/listings/search?q=cow&limit=20&offset=0", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3200", + "path": [ + "listings", + "search" + ], + "query": [ + { + "key": "q", + "value": "cow" + }, + { + "key": "limit", + "value": "20" + }, + { + "key": "offset", + "value": "0" + } + ] + }, + "description": "Search listings by text query" + } + }, + { + "name": "Near Me Search", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3200/listings/near-me?lat=18.5204&lng=73.8567&radius_meters=100000&limit=20", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3200", + "path": [ + "listings", + "near-me" + ], + "query": [ + { + "key": "lat", + "value": "18.5204" + }, + { + "key": "lng", + "value": "73.8567" + }, + { + "key": "radius_meters", + "value": "100000" + }, + { + "key": "limit", + "value": "20" + } + ] + }, + "description": "Find listings near a specific location" + } + }, + { + "name": "Get Species", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3200/listings/species", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3200", + "path": [ + "listings", + "species" + ] + }, + "description": "Get all available species" + } + }, + { + "name": "Get Breeds", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3200/listings/breeds?species_id=your-species-uuid", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3200", + "path": [ + "listings", + "breeds" + ], + "query": [ + { + "key": "species_id", + "value": "your-species-uuid", + "description": "Optional: Filter breeds by species" + } + ] + }, + "description": "Get all available breeds (optionally filtered by species)" + } + } + ] + }, + { + "name": "Locations", + "item": [ + { + "name": "Create Location", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"user_id\": \"aaf295cb-a19e-4179-a2df-31c0c64ea9f4\",\n \"lat\": 18.15,\n \"lng\": 74.5833,\n \"country\": \"India\",\n \"state\": \"Maharashtra\",\n \"district\": \"Pune\",\n \"city_village\": \"Baramati\",\n \"pincode\": \"413102\",\n \"location_type\": \"farm\",\n \"is_saved_address\": true,\n \"source_type\": \"manual\",\n \"source_confidence\": \"high\"\n}" + }, + "url": { + "raw": "http://localhost:3200/locations", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3200", + "path": [ + "locations" + ] + }, + "description": "Create a new location. user_id is optional for captured locations but required for saved addresses." + } + }, + { + "name": "Get User Locations", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3200/locations/user/:userId", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3200", + "path": [ + "locations", + "user", + ":userId" + ], + "variable": [ + { + "key": "userId", + "value": "aaf295cb-a19e-4179-a2df-31c0c64ea9f4" + } + ] + }, + "description": "Get all locations for a specific user" + } + }, + { + "name": "Get Location by ID", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3200/locations/:locationId", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3200", + "path": [ + "locations", + ":locationId" + ], + "variable": [ + { + "key": "locationId", + "value": "your-location-uuid-here" + } + ] + }, + "description": "Get a single location by ID" + } + }, + { + "name": "Update Location", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"city_village\": \"Updated City Name\",\n \"pincode\": \"413103\"\n}" + }, + "url": { + "raw": "http://localhost:3200/locations/:locationId", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3200", + "path": [ + "locations", + ":locationId" + ], + "variable": [ + { + "key": "locationId", + "value": "your-location-uuid-here" + } + ] + }, + "description": "Update location information" + } + } + ] + }, + { + "name": "Chat", + "item": [ + { + "name": "Create/Get Conversation", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"buyer_id\": \"buyer-uuid-here\",\n \"seller_id\": \"seller-uuid-here\"\n}" + }, + "url": { + "raw": "http://localhost:3200/chat/conversations", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3200", + "path": [ + "chat", + "conversations" + ] + }, + "description": "Create a new conversation or get existing one between buyer and seller" + } + }, + { + "name": "Get User Conversations", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3200/chat/conversations/user/:userId", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3200", + "path": [ + "chat", + "conversations", + "user", + ":userId" + ], + "variable": [ + { + "key": "userId", + "value": "aaf295cb-a19e-4179-a2df-31c0c64ea9f4" + } + ] + }, + "description": "Get all conversations for a specific user" + } + }, + { + "name": "Get Messages", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:3200/chat/conversations/:conversationId/messages?limit=50&offset=0", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3200", + "path": [ + "chat", + "conversations", + ":conversationId", + "messages" + ], + "query": [ + { + "key": "limit", + "value": "50" + }, + { + "key": "offset", + "value": "0" + } + ], + "variable": [ + { + "key": "conversationId", + "value": "your-conversation-uuid-here" + } + ] + }, + "description": "Get messages for a conversation" + } + }, + { + "name": "Send Message", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"conversation_id\": \"conversation-uuid-here\",\n \"sender_id\": \"sender-uuid-here\",\n \"receiver_id\": \"receiver-uuid-here\",\n \"content\": \"Hello, I'm interested in your listing.\",\n \"message_type\": \"text\"\n}" + }, + "url": { + "raw": "http://localhost:3200/chat/messages", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "3200", + "path": [ + "chat", + "messages" + ] + }, + "description": "Send a message in a conversation" + } + } + ] + } + ], + "variable": [ + { + "key": "baseUrl", + "value": "http://localhost:3200", + "type": "string" + } + ] +} + diff --git a/POSTMAN_TESTING_GUIDE.md b/POSTMAN_TESTING_GUIDE.md new file mode 100644 index 0000000..b81d0de --- /dev/null +++ b/POSTMAN_TESTING_GUIDE.md @@ -0,0 +1,143 @@ +# Postman Testing Guide for BuySellService API + +## šŸ“„ How to Import the Collection + +1. **Open Postman** +2. Click **Import** button (top left) +3. Select **File** tab +4. Choose `BuySellService_API.postman_collection.json` +5. Click **Import** + +The collection will appear in your Postman sidebar with all endpoints organized by category. + +## šŸš€ Quick Start Testing + +### Step 1: Create a User (Required First Step) + +Before creating listings or locations, you need to create a user: + +1. Go to **Users** → **Create User** +2. The request body is pre-filled with the UUID from your error: `aaf295cb-a19e-4179-a2df-31c0c64ea9f4` +3. Click **Send** +4. You should get a 201 response with the created user + +**Request Body:** +```json +{ + "id": "aaf295cb-a19e-4179-a2df-31c0c64ea9f4", + "name": "Test User", + "phone_number": "+919876543210", + "avatar_url": null, + "language": "en", + "timezone": "Asia/Kolkata", + "country_code": "+91" +} +``` + +### Step 2: Get Species and Breeds + +Before creating a listing, you need to know the UUIDs of species and breeds: + +1. Go to **Listings** → **Get Species** +2. Click **Send** - This will show you all available species with their UUIDs +3. Copy a species UUID +4. Go to **Listings** → **Get Breeds** +5. Add the species UUID as a query parameter: `?species_id=your-species-uuid` +6. Click **Send** - This will show breeds for that species +7. Copy a breed UUID + +### Step 3: Create a Location (Optional) + +1. Go to **Locations** → **Create Location** +2. Update the `user_id` in the request body to your user UUID +3. Modify other fields as needed +4. Click **Send** + +### Step 4: Create a Listing + +1. Go to **Listings** → **Create Listing** +2. Update the request body: + - `seller_id`: Use your user UUID + - `species_id`: Use the species UUID from Step 2 + - `breed_id`: Use the breed UUID from Step 2 +3. Click **Send** + +## šŸ“‹ Endpoint Categories + +### šŸ‘¤ Users +- **Create User** - Create a new user (with optional UUID) +- **Get All Users** - List all users +- **Get User by ID** - Get specific user details +- **Update User** - Update user information + +### šŸ“‹ Listings +- **Get All Listings** - List all active listings +- **Get Listing by ID** - Get specific listing with full details +- **Create Listing** - Create a new listing with animal details +- **Update Listing** - Update listing information +- **Search Listings** - Search by text query +- **Near Me Search** - Find listings near coordinates +- **Get Species** - Get all available species +- **Get Breeds** - Get all breeds (optionally filtered by species) + +### šŸ“ Locations +- **Create Location** - Create a location (user_id optional for captured locations) +- **Get User Locations** - Get all locations for a user +- **Get Location by ID** - Get specific location +- **Update Location** - Update location information + +### šŸ’¬ Chat +- **Create/Get Conversation** - Create or get existing conversation +- **Get User Conversations** - Get all conversations for a user +- **Get Messages** - Get messages in a conversation +- **Send Message** - Send a message + +## šŸ”§ Important Notes + +### UUIDs Required +- All IDs in this API are **UUIDs**, not integers +- Make sure to use valid UUID format: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx` + +### User ID Requirements +- **For Listings**: `seller_id` must exist in users table +- **For Locations**: + - `user_id` is **optional** if `is_saved_address = false` (captured location) + - `user_id` is **required** if `is_saved_address = true` (saved address) + +### Testing Order +1. āœ… Create User first +2. āœ… Get Species and Breeds +3. āœ… Create Location (optional) +4. āœ… Create Listing + +## šŸ› Common Errors + +### "User does not exist" +- **Solution**: Create the user first using the **Create User** endpoint + +### "Invalid input syntax for type uuid" +- **Solution**: Make sure you're using UUIDs, not integers. Check species_id and breed_id are UUIDs. + +### "Foreign key constraint violation" +- **Solution**: Ensure all referenced IDs (user_id, species_id, breed_id) exist in their respective tables + +## šŸ’” Tips + +1. **Use Environment Variables**: Create a Postman environment with: + - `baseUrl`: `http://localhost:3200` + - `userId`: Your user UUID + - `speciesId`: A species UUID + - `breedId`: A breed UUID + +2. **Save Responses**: After creating resources, save the returned UUIDs for use in other requests + +3. **Test in Order**: Follow the testing order above to avoid foreign key errors + +4. **Check Server**: Make sure your server is running on port 3200 before testing + +## šŸ”— Base URL + +All endpoints use: `http://localhost:3200` + +If your server runs on a different port, update the base URL in the collection variables. + diff --git a/package.json b/package.json index 1517bfd..340561b 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,9 @@ "type": "module", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node server.js", + "dev": "node server.js" }, "repository": { "type": "git", diff --git a/public/index.html b/public/index.html new file mode 100644 index 0000000..3b63070 --- /dev/null +++ b/public/index.html @@ -0,0 +1,1108 @@ + + + + + + BuySellService API Tester + + + +
+

šŸ„ BuySellService API Tester

+

Test your Livestock Marketplace API endpoints

+ + +
+

šŸ‘¤ Users API

+ +
+ Base URL: http://localhost:3200/users
+ Note: Create users before creating listings or saved addresses. +
+ +
+
+

Get All Users

+ +
+ +
+

Get Single User

+
+ + +
+ +
+
+ +

Create User

+
+ šŸ’” Quick Create: You can create a user with a specific UUID (like the one in your error) by providing the "id" field. +
+
+
+
+ + + + Use: aaf295cb-a19e-4179-a2df-31c0c64ea9f4 (from your error) + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +

Update User

+
+ + +
+
+ + +
+ + + +
+ + +
+

šŸ“‹ Listings API

+ +
+ Base URL: http://localhost:3200/listings
+ Note: Species ID and Breed ID must be UUIDs (not integers). Use the dropdowns below or enter UUIDs manually. +
+ +
+
+

Get Available Species

+ + +
+
+

Get Available Breeds

+
+ + +
+ + +
+
+ +
+
+

Get All Listings

+
+ + +
+ +
+ +
+

Get Single Listing

+
+ + +
+ +
+
+ +
+
+

Search Listings

+
+ + +
+ +
+ +
+

Near Me Search

+
+ + +
+
+ + +
+ +
+
+ +

Create Listing

+
+ āš ļø Important: The Seller ID must be a valid UUID that exists in the users table. + If you get a foreign key error, the user doesn't exist. You need to create the user first or use an existing user UUID. +
+
+ + + + Example: aaf295cb-a19e-4179-a2df-31c0c64ea9f4 + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + + Or enter UUID manually below +
+
+ + +
+
+ + + Or enter UUID manually below +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +

Update Listing

+
+ + +
+
+ + +
+
+ + +
+ + +

Delete Listing

+
+ + +
+ + + +
+ + +
+

šŸ“ Locations API

+ +
+ Base URL: http://localhost:3200/locations +
+ +
+
+

Get User Locations

+
+ + +
+ +
+ +
+

Get Single Location

+
+ + +
+ +
+
+ +

Create Location

+
+ ā„¹ļø Note: User ID is optional for captured locations (is_saved_address = false), + but required and must exist in users table for saved addresses (is_saved_address = true). +
+
+
+
+ + + + Required only if "Is Saved Address" is Yes + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +

Update Location

+
+ + +
+
+ + +
+ + + +
+ + +
+

šŸ’¬ Chat API

+ +
+ Base URL: http://localhost:3200/chat +
+ +
+
+

Create/Get Conversation

+
+ + +
+
+ + +
+ +
+ +
+

Get User Conversations

+
+ + +
+ +
+
+ +
+
+

Get Messages

+
+ + +
+ +
+ +
+

Send Message

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ + + + + diff --git a/routes/listingRoutes.js b/routes/listingRoutes.js index d0649d5..eae44c4 100644 --- a/routes/listingRoutes.js +++ b/routes/listingRoutes.js @@ -20,6 +20,20 @@ const createNewLocation = async (client, userId, locationData) => { pincode, } = locationData; + // Validate user exists if this is a saved address + let finalUserId = userId; + if (is_saved_address && userId) { + const userCheck = await client.query("SELECT id FROM users WHERE id = $1 AND deleted = FALSE", [userId]); + if (userCheck.rows.length === 0) { + throw new Error(`User with id ${userId} does not exist. Cannot create saved address for non-existent user.`); + } + } else if (!is_saved_address) { + // For captured locations (not saved addresses), user_id can be NULL + finalUserId = null; + } else if (is_saved_address && !userId) { + throw new Error("user_id is required when is_saved_address is true"); + } + // 1. Insert into locations (Merged Table) const insertLocationQuery = ` INSERT INTO locations ( @@ -31,7 +45,7 @@ const createNewLocation = async (client, userId, locationData) => { RETURNING id `; const locationValues = [ - userId, + finalUserId, lat, lng, source_type || "manual", @@ -266,6 +280,18 @@ router.post("/", async (req, res) => { media // Array of { media_url, media_type, is_primary, sort_order } } = req.body; + // Validate seller exists + if (seller_id) { + const sellerCheck = await client.query("SELECT id FROM users WHERE id = $1 AND deleted = FALSE", [seller_id]); + if (sellerCheck.rows.length === 0) { + await client.query("ROLLBACK"); + return res.status(400).json({ error: `Seller with id ${seller_id} does not exist. Please create the user first.` }); + } + } else { + await client.query("ROLLBACK"); + return res.status(400).json({ error: "seller_id is required" }); + } + let final_location_id = animal?.location_id; // 1. Create Location (if needed) @@ -531,11 +557,32 @@ router.delete("/:id", async (req, res) => { }); +// Get the list of species +router.get("/species", async (req, res) => { + try { + const queryText = "SELECT * FROM species WHERE deleted = FALSE ORDER BY name"; + const result = await pool.query(queryText); + res.json(result.rows); + } catch (err) { + console.error("Error fetching species:", 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); + const { species_id } = req.query; + let queryText = "SELECT * FROM breeds WHERE deleted = FALSE"; + const queryParams = []; + + if (species_id) { + queryText += " AND species_id = $1"; + queryParams.push(species_id); + } + + queryText += " ORDER BY name"; + const result = await pool.query(queryText, queryParams); res.json(result.rows); } catch (err) { console.error("Error fetching breeds:", err); diff --git a/routes/locationRoutes.js b/routes/locationRoutes.js index 3d5b35c..048a027 100644 --- a/routes/locationRoutes.js +++ b/routes/locationRoutes.js @@ -25,6 +25,26 @@ router.post("/", async (req, res) => { pincode, } = req.body; + // Validate user exists if this is a saved address + let finalUserId = user_id; + if (is_saved_address && user_id) { + const userCheck = await client.query("SELECT id FROM users WHERE id = $1 AND deleted = FALSE", [user_id]); + if (userCheck.rows.length === 0) { + await client.query("ROLLBACK"); + return res.status(400).json({ + error: `User with id ${user_id} does not exist. Cannot create saved address for non-existent user.` + }); + } + } else if (!is_saved_address) { + // For captured locations (not saved addresses), user_id can be NULL + finalUserId = null; + } else if (is_saved_address && !user_id) { + await client.query("ROLLBACK"); + return res.status(400).json({ + error: "user_id is required when is_saved_address is true" + }); + } + // 1. Insert into locations const insertLocationQuery = ` INSERT INTO locations ( @@ -35,7 +55,7 @@ router.post("/", async (req, res) => { RETURNING * `; const locationValues = [ - user_id, + finalUserId, lat, lng, source_type || "manual", @@ -59,7 +79,15 @@ router.post("/", async (req, res) => { } catch (err) { await client.query("ROLLBACK"); console.error("Error creating location:", err); - res.status(500).json({ error: "Internal server error" }); + + // Provide more specific error messages + if (err.code === '23503') { // Foreign key violation + return res.status(400).json({ + error: `Foreign key constraint violation: ${err.detail || 'The provided user_id does not exist in the users table.'}` + }); + } + + res.status(500).json({ error: err.message || "Internal server error" }); } finally { client.release(); } diff --git a/routes/userRoutes.js b/routes/userRoutes.js new file mode 100644 index 0000000..d6aa12c --- /dev/null +++ b/routes/userRoutes.js @@ -0,0 +1,169 @@ +import express from "express"; +import pool from "../db/pool.js"; + +const router = express.Router(); + +// 1. CREATE User +router.post("/", async (req, res) => { + const client = await pool.connect(); + try { + await client.query("BEGIN"); + + const { + id, // Optional: if provided, use this UUID; otherwise generate one + name, + phone_number, + avatar_url, + language, + timezone, + country_code = "+91", + } = req.body; + + // If id is provided, use it; otherwise let the database generate one + const insertUserQuery = id + ? ` + INSERT INTO users (id, name, phone_number, avatar_url, language, timezone, country_code) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING * + ` + : ` + INSERT INTO users (name, phone_number, avatar_url, language, timezone, country_code) + VALUES ($1, $2, $3, $4, $5, $6) + RETURNING * + `; + + const userValues = id + ? [id, name, phone_number, avatar_url, language, timezone, country_code] + : [name, phone_number, avatar_url, language, timezone, country_code]; + + const userResult = await client.query(insertUserQuery, userValues); + + await client.query("COMMIT"); + + res.status(201).json(userResult.rows[0]); + } catch (err) { + await client.query("ROLLBACK"); + console.error("Error creating user:", err); + + if (err.code === "23505") { + // Unique constraint violation + return res.status(400).json({ + error: err.detail || "A user with this phone number or ID already exists", + }); + } + + if (err.code === "23503") { + // Foreign key violation + return res.status(400).json({ + error: err.detail || "Foreign key constraint violation", + }); + } + + res.status(500).json({ error: err.message || "Internal server error" }); + } finally { + client.release(); + } +}); + +// 2. GET All Users +router.get("/", async (req, res) => { + try { + const { limit = 100, offset = 0 } = req.query; + const queryText = ` + SELECT id, name, phone_number, avatar_url, language, timezone, country_code, + is_active, created_at, updated_at + FROM users + WHERE deleted = FALSE + ORDER BY created_at DESC + LIMIT $1 OFFSET $2 + `; + const result = await pool.query(queryText, [limit, offset]); + res.json(result.rows); + } catch (err) { + console.error("Error fetching users:", err); + res.status(500).json({ error: "Internal server error" }); + } +}); + +// 3. GET Single User +router.get("/:id", async (req, res) => { + try { + const { id } = req.params; + const queryText = ` + SELECT id, name, phone_number, avatar_url, language, timezone, country_code, + is_active, created_at, updated_at + FROM users + WHERE id = $1 AND deleted = FALSE + `; + const result = await pool.query(queryText, [id]); + + if (result.rows.length === 0) { + return res.status(404).json({ error: "User not found" }); + } + + res.json(result.rows[0]); + } catch (err) { + console.error("Error fetching user:", err); + res.status(500).json({ error: "Internal server error" }); + } +}); + +// 4. UPDATE User +router.put("/:id", async (req, res) => { + try { + const { id } = req.params; + const { name, phone_number, avatar_url, language, timezone, country_code, is_active } = req.body; + + const updateQuery = ` + UPDATE users + SET name = COALESCE($1, name), + phone_number = COALESCE($2, phone_number), + avatar_url = COALESCE($3, avatar_url), + language = COALESCE($4, language), + timezone = COALESCE($5, timezone), + country_code = COALESCE($6, country_code), + is_active = COALESCE($7, is_active) + WHERE id = $8 AND deleted = FALSE + RETURNING * + `; + + const values = [name, phone_number, avatar_url, language, timezone, country_code, is_active, id]; + + const result = await pool.query(updateQuery, values); + + if (result.rows.length === 0) { + return res.status(404).json({ error: "User not found" }); + } + + res.json(result.rows[0]); + } catch (err) { + console.error("Error updating user:", err); + res.status(500).json({ error: "Internal server error" }); + } +}); + +// 5. DELETE User (Soft Delete) +router.delete("/:id", async (req, res) => { + try { + const { id } = req.params; + const queryText = ` + UPDATE users + SET deleted = TRUE + WHERE id = $1 + RETURNING id + `; + const result = await pool.query(queryText, [id]); + + if (result.rows.length === 0) { + return res.status(404).json({ error: "User not found" }); + } + + res.json({ message: "User deleted successfully" }); + } catch (err) { + console.error("Error deleting user:", err); + res.status(500).json({ error: "Internal server error" }); + } +}); + +export default router; + diff --git a/server.js b/server.js index b72fe81..a91f5f1 100644 --- a/server.js +++ b/server.js @@ -1,24 +1,31 @@ import express from "express"; import cors from "cors"; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; +import http from "http"; +import listingRoutes from "./routes/listingRoutes.js"; +import locationRoutes from "./routes/locationRoutes.js"; +import chatRoutes from "./routes/chatRoutes.js"; +import userRoutes from "./routes/userRoutes.js"; +import { initSocket } from "./socket.js"; +import { startExpirationJob } from "./jobs/expirationJob.js"; const app = express(); app.use(cors()); app.use(express.json()); +// Serve static files from public directory +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +app.use(express.static(join(__dirname, 'public'))); + const PORT = process.env.PORT || 3200; // Add routes here -import listingRoutes from "./routes/listingRoutes.js"; -import locationRoutes from "./routes/locationRoutes.js"; -import chatRoutes from "./routes/chatRoutes.js"; - - app.use("/listings", listingRoutes); app.use("/locations", locationRoutes); app.use("/chat", chatRoutes); -import http from "http"; -import { initSocket } from "./socket.js"; -import { startExpirationJob } from "./jobs/expirationJob.js"; +app.use("/users", userRoutes); const server = http.createServer(app); initSocket(server);