import { Router } from 'express'; import { z } from 'zod'; import { pool } from '../db/connection.js'; import { requireAuth } from '../middleware/auth.js'; import { requireIdempotencyKey } from '../middleware/idempotency.js'; const router = Router(); // Zod schema for offer creation validation const createOfferSchema = z.object({ amountChf: z.number().positive(), message: z.string().max(2000).optional() }); // Zod schema for negotiation validation const negotiateSchema = z.object({ amountChf: z.number().positive(), message: z.string().max(2000).optional() }); router.post('/:requestId', requireAuth, requireIdempotencyKey, async (req, res) => { try { const requestId = Number(req.params.requestId); if (Number.isNaN(requestId)) { return res.status(400).json({ error: 'Invalid requestId' }); } const parsed = createOfferSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: 'Invalid payload', details: parsed.error.flatten() }); } const { amountChf, message } = parsed.data; const [result] = await pool.query( `INSERT INTO offers (request_id, helper_id, amount_chf, message) VALUES (?, ?, ?, ?)`, [requestId, req.user.userId, amountChf, message || null] ); await pool.query('UPDATE help_requests SET status = ? WHERE id = ?', ['negotiating', requestId]); // Cache the response for idempotent requests if (req.cacheIdempotentResponse) { await req.cacheIdempotentResponse(201, { id: result.insertId }); } res.status(201).json({ id: result.insertId }); } catch (error) { console.error('Error in POST /offers/:requestId:', error); res.status(500).json({ error: 'Internal server error' }); } }); router.post('/negotiation/:offerId', requireAuth, requireIdempotencyKey, async (req, res) => { try { const offerId = Number(req.params.offerId); if (Number.isNaN(offerId)) { return res.status(400).json({ error: 'Invalid offerId' }); } const parsed = negotiateSchema.safeParse(req.body); if (!parsed.success) { return res.status(400).json({ error: 'Invalid payload', details: parsed.error.flatten() }); } const { amountChf, message } = parsed.data; const [result] = await pool.query( `INSERT INTO negotiations (offer_id, sender_id, amount_chf, message) VALUES (?, ?, ?, ?)`, [offerId, req.user.userId, amountChf, message || null] ); await pool.query('UPDATE offers SET status = ? WHERE id = ?', ['countered', offerId]); // Cache the response for idempotent requests if (req.cacheIdempotentResponse) { await req.cacheIdempotentResponse(201, { id: result.insertId }); } res.status(201).json({ id: result.insertId }); } catch (error) { console.error('Error in POST /offers/negotiation/:offerId:', error); res.status(500).json({ error: 'Internal server error' }); } }); router.post('/accept/:offerId', requireAuth, requireIdempotencyKey, async (req, res) => { try { const offerId = Number(req.params.offerId); if (Number.isNaN(offerId)) { return res.status(400).json({ error: 'Invalid offerId' }); } const [offers] = await pool.query( 'SELECT id, request_id, amount_chf FROM offers WHERE id = ? LIMIT 1', [offerId] ); const offer = offers[0]; if (!offer) { return res.status(404).json({ error: 'Offer not found' }); } await pool.query('UPDATE offers SET status = ? WHERE id = ?', ['accepted', offerId]); await pool.query('UPDATE help_requests SET status = ? WHERE id = ?', ['agreed', offer.request_id]); const [dealResult] = await pool.query( 'INSERT INTO deals (request_id, offer_id, agreed_amount_chf) VALUES (?, ?, ?)', [offer.request_id, offer.id, offer.amount_chf] ); // Cache the response for idempotent requests if (req.cacheIdempotentResponse) { await req.cacheIdempotentResponse(201, { dealId: dealResult.insertId }); } res.status(201).json({ dealId: dealResult.insertId }); } catch (error) { console.error('Error in POST /offers/accept/:offerId:', error); res.status(500).json({ error: 'Internal server error' }); } }); export default router;