File size: 4,649 Bytes
54d9dcb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
'use strict';

const { v4: uuidv4 } = require('uuid');
const { getPool } = require('../db');

/**
 * Create a new feedback event (PostgreSQL)
 */
async function createFeedbackEvent(req, res) {
  const {
    event_type,
    session_id,
    user_hash,
    context = {},
    itinerary_snapshot = [],
    modifications = [],
    reward = 0
  } = req.body;

  // Validation
  if (!event_type) {
    return res.status(400).json({ 
      error: { message: 'event_type is required' } 
    });
  }

  const validEventTypes = [
    'plan_saved', 'plan_completed', 'rating_positive',
    'poi_added', 'plan_abandoned', 'poi_removed',
    'rating_negative', 'poi_reordered', 'poi_swapped', 'session_start'
  ];

  if (!validEventTypes.includes(event_type)) {
    return res.status(400).json({ 
      error: { message: `Invalid event_type. Must be one of: ${validEventTypes.join(', ')}` } 
    });
  }

  try {
    const pool = getPool();
    if (!pool) {
      return res.status(503).json({ error: { message: 'Database not available' } });
    }

    const id = uuidv4();
    const query = `
      INSERT INTO feedback_events 
      (id, event_type, session_id, user_hash, context, itinerary_snapshot, modifications, reward, created_at)
      VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW())
    `;
    
    const params = [
      id,
      event_type,
      session_id || null,
      user_hash || null,
      JSON.stringify(context),
      JSON.stringify(itinerary_snapshot),
      JSON.stringify(modifications),
      reward
    ];

    await pool.query(query, params);

    res.status(201).json({ 
      data: { id, event_type, session_id, reward },
      message: 'Feedback event recorded successfully'
    });
  } catch (err) {
    console.error('Error creating feedback event:', err);
    res.status(500).json({ error: { message: 'Failed to record feedback event' } });
  }
}

/**
 * List feedback events with optional filters (PostgreSQL)
 */
async function listFeedbackEvents(req, res) {
  const { event_type, session_id, limit = 100, offset = 0 } = req.query;

  try {
    const pool = getPool();
    if (!pool) {
      return res.status(503).json({ error: { message: 'Database not available' } });
    }

    let query = 'SELECT * FROM feedback_events WHERE 1=1';
    const params = [];
    let paramCounter = 1;

    if (event_type) {
      query += ` AND event_type = $${paramCounter++}`;
      params.push(event_type);
    }

    if (session_id) {
      query += ` AND session_id = $${paramCounter++}`;
      params.push(session_id);
    }

    query += ` ORDER BY created_at DESC LIMIT $${paramCounter++} OFFSET $${paramCounter++}`;
    params.push(Math.min(parseInt(limit), 1000), parseInt(offset));

    const result = await pool.query(query, params);

    res.json({ data: result.rows });
  } catch (err) {
    console.error('Error listing feedback events:', err);
    res.status(500).json({ error: { message: 'Failed to list feedback events' } });
  }
}

/**
 * Get aggregated statistics for a POI (PostgreSQL)
 */
async function getPOIStats(req, res) {
  const { poi_id } = req.params;

  if (!poi_id) {
    return res.status(400).json({ error: { message: 'poi_id is required' } });
  }

  try {
    const pool = getPool();
    if (!pool) {
      return res.status(503).json({ error: { message: 'Database not available' } });
    }

    // PostgreSQL JSONB search or simple LIKE on TEXT
    const query = `
      SELECT event_type, reward, modifications 
      FROM feedback_events 
      WHERE itinerary_snapshot::text LIKE $1
    `;
    
    const result = await pool.query(query, [`%${poi_id}%`]);
    const events = result.rows;

    const stats = {
      poi_id,
      times_suggested: events.length,
      times_saved: events.filter(e => e.event_type === 'plan_saved').length,
      times_removed: events.filter(e => {
        if (e.event_type !== 'poi_removed') return false;
        const mods = typeof e.modifications === 'string' ? JSON.parse(e.modifications) : e.modifications;
        return Array.isArray(mods) && mods.some(m => m.poi_id === poi_id);
      }).length,
      avg_reward: events.length > 0 
        ? (events.reduce((sum, e) => sum + parseFloat(e.reward), 0) / events.length).toFixed(2)
        : 0,
      save_rate: 0
    };

    if (stats.times_suggested > 0) {
      stats.save_rate = (stats.times_saved / stats.times_suggested).toFixed(2);
    }

    res.json({ data: stats });
  } catch (err) {
    console.error('Error getting POI stats:', err);
    res.status(500).json({ error: { message: 'Failed to get POI stats' } });
  }
}

module.exports = {
  createFeedbackEvent,
  listFeedbackEvents,
  getPOIStats
};