import express from "express"; import pool from "../db/pool.js"; import { getIO, getSocketId } from "../socket.js"; // import { sendPushNotification } from "../utils/fcm.js"; const router = express.Router(); // 1. POST /conversations (Create or Get existing) router.post("/conversations", async (req, res) => { try { // We expect explicit buyer/seller ID, or just two user IDs. // Based on schema, we have buyer_id and seller_id. // Ideally, the client sends which is which, or we just treat them as user1/user2 and check both permutations. const { buyer_id, seller_id } = req.body; if (!buyer_id || !seller_id) { return res.status(400).json({ error: "buyer_id and seller_id are required" }); } // Check if conversation exists (bidirectional check for robustness, although schema has specific columns) const queryCheck = ` SELECT * FROM conversations WHERE (buyer_id = $1 AND seller_id = $2) OR (buyer_id = $2 AND seller_id = $1) AND deleted = FALSE `; const checkResult = await pool.query(queryCheck, [buyer_id, seller_id]); if (checkResult.rows.length > 0) { return res.json(checkResult.rows[0]); } // Create new const queryInsert = ` INSERT INTO conversations (buyer_id, seller_id) VALUES ($1, $2) RETURNING * `; const insertResult = await pool.query(queryInsert, [buyer_id, seller_id]); res.status(201).json(insertResult.rows[0]); } catch (err) { console.error("Error creating/getting conversation:", err); res.status(500).json({ error: "Internal server error" }); } }); // 2. GET /conversations/user/:userId (Inbox) router.get("/conversations/user/:userId", async (req, res) => { try { const { userId } = req.params; // Fetch conversations where user is involved. // Also fetch the OTHER user's name/avatar. const queryText = ` SELECT c.*, CASE WHEN c.buyer_id = $1 THEN u_seller.name ELSE u_buyer.name END as other_user_name, CASE WHEN c.buyer_id = $1 THEN u_seller.avatar_url ELSE u_buyer.avatar_url END as other_user_avatar, CASE WHEN c.buyer_id = $1 THEN u_seller.id ELSE u_buyer.id END as other_user_id FROM conversations c JOIN users u_buyer ON c.buyer_id = u_buyer.id JOIN users u_seller ON c.seller_id = u_seller.id WHERE (c.buyer_id = $1 OR c.seller_id = $1) AND c.deleted = FALSE ORDER BY c.updated_at DESC `; const result = await pool.query(queryText, [userId]); res.json(result.rows); } catch (err) { console.error("Error fetching user conversations:", err); res.status(500).json({ error: "Internal server error" }); } }); // 3. GET /conversations/:conversationId/messages (History) router.get("/conversations/:conversationId/messages", async (req, res) => { try { const { conversationId } = req.params; const { limit = 50, offset = 0 } = req.query; const queryText = ` SELECT m.* FROM messages m WHERE m.conversation_id = $1 AND m.deleted = FALSE ORDER BY m.created_at DESC LIMIT $2 OFFSET $3 `; const result = await pool.query(queryText, [conversationId, limit, offset]); // Reverse for frontend if needed, but API usually sends standard order. // Sending newest first (DESC) is common for pagination. res.json(result.rows); } catch (err) { console.error("Error fetching messages:", err); res.status(500).json({ error: "Internal server error" }); } }); // 4. POST /messages (Send Message) router.post("/messages", async (req, res) => { const client = await pool.connect(); try { await client.query("BEGIN"); const { conversation_id, sender_id, receiver_id, content, message_type = 'text', media_url, media_type, media_metadata } = req.body; // Insert Message with embedded media fields const insertMessageQuery = ` INSERT INTO messages ( conversation_id, sender_id, receiver_id, message_type, content, message_media, media_type, media_metadata ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING * `; const messageResult = await client.query(insertMessageQuery, [ conversation_id, sender_id, receiver_id, message_type, content, media_url || null, media_type || null, media_metadata || null ]); await client.query("COMMIT"); // Real-time update via Socket.io const receiverSocketId = getSocketId(receiver_id); if (receiverSocketId) { getIO().to(receiverSocketId).emit("receive_message", messageResult.rows[0]); } // else { // // Receiver is OFFLINE: Send Push Notification // const fcmQuery = `SELECT fcm_token FROM user_devices WHERE user_id = $1 AND fcm_token IS NOT NULL AND is_active = TRUE`; // const fcmResult = await pool.query(fcmQuery, [receiver_id]); // if (fcmResult.rows.length > 0) { // const tokens = fcmResult.rows.map(row => row.fcm_token); // // Title could be sender's name if we fetched it, or generic. For speed, generic "New Message". // // Ideally, we'd join sender info in the SELECT or pass it if available. // const notificationTitle = "New Message"; // const notificationBody = message_type === 'text' ? (content.substring(0, 100) + (content.length > 100 ? "..." : "")) : "Sent a media file"; // sendPushNotification(tokens, notificationTitle, notificationBody, { // type: "new_message", // conversation_id: conversation_id, // message_id: messageResult.rows[0].id // }); // } // } res.status(201).json(messageResult.rows[0]); } catch (err) { await client.query("ROLLBACK"); console.error("Error sending message:", err); res.status(500).json({ error: "Internal server error" }); } finally { client.release(); } }); // 5. PUT /conversations/:conversationId/read (Mark Conversation as Read) router.put("/conversations/:conversationId/read", async (req, res) => { try { const { conversationId } = req.params; const { userId } = req.body; // The user who is reading the messages if (!userId) { return res.status(400).json({ error: "userId is required" }); } const queryText = ` UPDATE messages SET is_read = TRUE, read_at = NOW() WHERE conversation_id = $1 AND receiver_id = $2 AND is_read = FALSE RETURNING id, sender_id, conversation_id `; const result = await pool.query(queryText, [conversationId, userId]); // Even if 0 rows updated, we return success (idempotent) res.json({ message: "Messages marked as read", updated_count: result.rows.length, updated_messages: result.rows }); // Real-time update via Socket.io // Notify the SENDER(s) that their messages have been read. // In a 1-on-1 chat, this is just one person. if (result.rows.length > 0) { const uniqueSenders = [...new Set(result.rows.map(m => m.sender_id))]; for (const senderId of uniqueSenders) { const senderSocketId = getSocketId(senderId); if (senderSocketId) { getIO().to(senderSocketId).emit("conversation_read", { conversation_id: conversationId, read_by_user_id: userId, updated_count: result.rows.length // Simplification: total count, not per sender, but fine for 1:1 }); } } } } catch (err) { console.error("Error marking conversation as read:", err); res.status(500).json({ error: "Internal server error" }); } }); // 6. POST /communications (Log Call/Communication) router.post("/communications", async (req, res) => { const client = await pool.connect(); try { const { conversation_id, buyer_id, seller_id, communication_type, call_status, duration_seconds, call_recording_url } = req.body; await client.query("BEGIN"); const insertQuery = ` INSERT INTO communication_records ( conversation_id, buyer_id, seller_id, communication_type, call_status, duration_seconds, call_recording_url ) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING * `; const result = await client.query(insertQuery, [ conversation_id, buyer_id, seller_id, communication_type, call_status, duration_seconds || 0, call_recording_url ]); await client.query("COMMIT"); res.status(201).json(result.rows[0]); } catch (err) { await client.query("ROLLBACK"); console.error("Error logging communication:", err); res.status(500).json({ error: "Internal server error" }); } finally { client.release(); } }); export default router;