- Timestamp:
- 06/07/26 21:34:27 (5 weeks ago)
- Branches:
- main
- Children:
- 679113b
- Parents:
- 3679996
- git-author:
- Marija Taseska <108740625+marijataseska@…> (06/07/26 21:34:27)
- git-committer:
- GitHub <noreply@…> (06/07/26 21:34:27)
- File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
server.js
r3679996 rb7d8a4d 61 61 port: parseInt(process.env.DB_PORT, 10), 62 62 ssl: { rejectUnauthorized: false } 63 }); 64 65 // POST /api/weddings/:wid/guests/bulk — add multiple guests in a transaction 66 app.post('/api/weddings/:wid/guests/bulk', requireAuth, async (req, res) => { 67 const { guests, sendEmails } = req.body; 68 if (!Array.isArray(guests) || guests.length === 0) return res.status(400).json({ error: 'guests array is required.' }); 69 70 const wid = req.params.wid; 71 const client = await pool.connect(); 72 try { 73 // Ensure wedding exists 74 const wRes = await client.query('SET search_path TO project; SELECT wedding_id, "date" FROM wedding WHERE wedding_id=$1', [wid]); 75 if (wRes.rows.length === 0) { 76 client.release(); 77 return res.status(404).json({ error: 'Wedding not found.' }); 78 } 79 80 // Fetch events for this wedding 81 const evRes = await client.query('SELECT event_id, event_type, "date", start_time, end_time FROM project.event WHERE wedding_id=$1 ORDER BY "date", start_time', [wid]); 82 const events = evRes.rows; 83 84 await client.query('BEGIN'); 85 const inserted = []; 86 for (const g of guests) { 87 const first_name = (g.first_name || '').trim(); 88 const last_name = (g.last_name || '').trim(); 89 const email = (g.email || null); 90 const role = (g.role || null); 91 if (!first_name || !last_name) { 92 await client.query('ROLLBACK'); 93 return res.status(400).json({ error: 'Each guest must have first_name and last_name.' }); 94 } 95 const ins = await client.query('INSERT INTO project.guest(first_name,last_name,email,wedding_id,role) VALUES ($1,$2,$3,$4,$5) RETURNING *', [first_name, last_name, email, wid, role]); 96 const guest = ins.rows[0]; 97 inserted.push(guest); 98 99 // Create RSVP records for each event 100 for (const ev of events) { 101 try { 102 await client.query( 103 `INSERT INTO project.event_rsvp(status, response_date, guest_id, event_id) 104 VALUES ('invited', CURRENT_DATE, $1, $2) 105 ON CONFLICT (guest_id, event_id) DO NOTHING`, 106 [guest.guest_id, ev.event_id] 107 ); 108 } catch (e) { 109 // Log and continue 110 console.warn('Could not create RSVP for guest', guest.guest_id, 'event', ev.event_id, e.message); 111 } 112 } 113 } 114 115 await client.query('COMMIT'); 116 117 // Optionally send emails after commit 118 if (sendEmails) { 119 for (const guest of inserted) { 120 if (!guest.email) continue; 121 try { 122 // Build event links 123 const eventLinks = events.map(ev => { 124 const token = crypto.createHmac('sha256', 'wedding_rsvp_secret').update(`${guest.guest_id}-${ev.event_id}`).digest('hex'); 125 const base = `http://localhost:${PORT}`; 126 return ` 127 <tr> 128 <td style="padding:8px 0; font-size:15px; color:#2c1f1f;"> 129 <strong>${ev.event_type}</strong> — ${ev.date} ${ev.start_time}–${ev.end_time} 130 </td> 131 <td style="padding:8px 0; text-align:right;"> 132 <a href="${base}/rsvp.html?guest=${guest.guest_id}&event=${ev.event_id}&token=${token}&action=accepted" style="background:#3a9e6b;color:#fff;padding:6px 14px;border-radius:6px;text-decoration:none;margin-right:6px;font-size:13px;">✅ Accept</a> 133 <a href="${base}/rsvp.html?guest=${guest.guest_id}&event=${ev.event_id}&token=${token}&action=declined" style="background:#c94545;color:#fff;padding:6px 14px;border-radius:6px;text-decoration:none;font-size:13px;">❌ Decline</a> 134 </td> 135 </tr>`; 136 }).join(''); 137 138 const mailOptions = { 139 from: `"Wedding Planner" <${process.env.EMAIL_USER}>`, 140 to: guest.email, 141 subject: `You're Invited! 💍 ${req.session.user.first_name} & ${req.session.user.last_name}'s Wedding`, 142 html: ` 143 <div style="font-family:'DM Sans',Arial,sans-serif;max-width:600px;margin:auto;background:#fdf8f3;border-radius:12px;overflow:hidden;border:1px solid #ecddd0;"> 144 <div style="background:linear-gradient(135deg,#d4606a,#e8949c);padding:32px;text-align:center;"> 145 <div style="font-size:36px;margin-bottom:8px;">💍</div> 146 <h1 style="color:#fff;font-size:26px;margin:0;">You're Invited!</h1> 147 <p style="color:rgba(255,255,255,0.9);margin:8px 0 0;">Wedding Invitation</p> 148 </div> 149 <div style="padding:32px;"> 150 <p style="font-size:16px;color:#2c1f1f;">Dear <strong>${guest.first_name} ${guest.last_name}</strong>,</p> 151 <p style="font-size:15px;color:#6b4f4f;line-height:1.6;">Please RSVP for the events below:</p> 152 <table style="width:100%;border-collapse:collapse;">${eventLinks}</table> 153 </div> 154 <div style="background:#f2e8db;padding:16px;text-align:center;"></div> 155 </div>` 156 }; 157 await transporter.sendMail(mailOptions); 158 } catch (mailErr) { 159 console.warn('Failed to send invite to', guest.email, mailErr.message); 160 } 161 } 162 } 163 164 client.release(); 165 res.json({ inserted: inserted.length, guests: inserted }); 166 } catch (err) { 167 try { await client.query('ROLLBACK'); } catch (e) { /* ignore */ } 168 client.release(); 169 console.error('Bulk guest insert error:', err.message); 170 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message }); 171 res.status(500).json({ error: 'Server error during bulk guest insert.' }); 172 } 63 173 }); 64 174 … … 83 193 84 194 // ============================================================= 195 // Service layer: Booking validation utilities 196 // ============================================================= 197 class ApiError extends Error { 198 constructor(status, message) { 199 super(message); 200 this.status = status; 201 } 202 } 203 204 const BookingService = { 205 // Normalize date value (DATE from PG may be string or Date) 206 _fmtDate(d) { 207 if (!d) return null; 208 if (d instanceof Date) return d.toISOString().slice(0,10); 209 // assume string YYYY-MM-DD 210 return String(d).slice(0,10); 211 }, 212 213 // Check if two time intervals overlap: [s1,e1) and [s2,e2) 214 isOverlapping(s1, e1, s2, e2) { 215 if (!s1 || !e1 || !s2 || !e2) return false; 216 // times are strings like HH:MM:SS or HH:MM 217 const toSec = t => { 218 const parts = String(t).split(':').map(Number); 219 return (parts[0]||0)*3600 + (parts[1]||0)*60 + (parts[2]||0); 220 }; 221 const a1 = toSec(s1), b1 = toSec(e1), a2 = toSec(s2), b2 = toSec(e2); 222 return a1 < b2 && a2 < b1; 223 }, 224 225 // Gather all bookings/events for a given wedding and date 226 async getBookingsForWeddingDate(wedding_id, date) { 227 const q = ` 228 SELECT 'venue' AS type, booking_id AS id, start_time, end_time 229 FROM project.venue_booking WHERE wedding_id=$1 AND "date"=$2 230 UNION ALL 231 SELECT 'church' AS type, booking_id AS id, start_time, end_time 232 FROM project.church_booking WHERE wedding_id=$1 AND "date"=$2 233 UNION ALL 234 SELECT 'registrar' AS type, booking_id AS id, start_time, end_time 235 FROM project.registrar_booking WHERE wedding_id=$1 AND "date"=$2 236 UNION ALL 237 SELECT 'photographer' AS type, booking_id AS id, start_time, end_time 238 FROM project.photographer_booking WHERE wedding_id=$1 AND "date"=$2 239 UNION ALL 240 SELECT 'band' AS type, booking_id AS id, start_time, end_time 241 FROM project.band_booking WHERE wedding_id=$1 AND "date"=$2 242 UNION ALL 243 SELECT 'event' AS type, event_id AS id, start_time, end_time 244 FROM project.event WHERE wedding_id=$1 AND "date"=$2 245 `; 246 const res = await pool.query(q, [wedding_id, date]); 247 return res.rows; 248 }, 249 250 // Validate booking date equals wedding date 251 async validateDateMatchesWedding(wedding_id, date) { 252 // Use SQL date cast/comparison to avoid client-side format issues 253 const r = await pool.query('SELECT 1 FROM project.wedding WHERE wedding_id=$1 AND "date" = $2::date', [wedding_id, date]); 254 if (r.rows.length === 0) { 255 // Determine whether wedding not found or date mismatch 256 const exists = await pool.query('SELECT 1 FROM project.wedding WHERE wedding_id=$1', [wedding_id]); 257 if (exists.rows.length === 0) throw new ApiError(404, 'Wedding not found.'); 258 throw new ApiError(400, 'Booking is only allowed on the wedding date.'); 259 } 260 }, 261 262 // Validate time slot overlap across all bookings for the wedding/date 263 // exclude optional { type, id } to allow updating existing record 264 async validateNoOverlap(wedding_id, date, start_time, end_time, exclude) { 265 const bookings = await BookingService.getBookingsForWeddingDate(wedding_id, date); 266 for (const b of bookings) { 267 if (exclude && String(exclude.type) === String(b.type) && String(exclude.id) === String(b.id)) continue; 268 if (BookingService.isOverlapping(start_time, end_time, b.start_time, b.end_time)) { 269 throw new ApiError(409, 'Time slot conflict detected: this booking overlaps with another scheduled event.'); 270 } 271 } 272 }, 273 274 // Registrar location must match wedding venue location for that wedding date 275 // We require an existing venue booking on the wedding date and compare locations 276 async validateRegistrarLocation(wedding_id, date, registrar_id) { 277 // find registrar 278 const r = await pool.query('SELECT location FROM project.registrar WHERE registrar_id=$1', [registrar_id]); 279 if (r.rows.length === 0) throw new ApiError(404, 'Registrar not found.'); 280 const regLoc = r.rows[0].location; 281 282 // find venue booking(s) for the wedding on the date 283 const vb = await pool.query('SELECT v.location FROM project.venue_booking vb JOIN project.venue v ON vb.venue_id=v.venue_id WHERE vb.wedding_id=$1 AND vb."date"=$2', [wedding_id, date]); 284 if (vb.rows.length === 0) { 285 // no venue booked on that date -> not allowed 286 throw new ApiError(400, 'Registrar booking is only allowed at the wedding venue location.'); 287 } 288 // require at least one matching venue location 289 const match = vb.rows.some(x => String(x.location).trim().toLowerCase() === String(regLoc).trim().toLowerCase()); 290 if (!match) throw new ApiError(400, 'Registrar booking is only allowed at the wedding venue location.'); 291 } 292 }; 293 294 // ============================================================= 85 295 // EMAIL TRANSPORTER 86 296 // ============================================================= … … 323 533 return res.status(400).json({ error: 'event_type, date, start_time, end_time are required.' }); 324 534 try { 535 // Business validations 536 await BookingService.validateDateMatchesWedding(req.params.wid, date); 537 await BookingService.validateNoOverlap(req.params.wid, date, start_time, end_time, null); 538 325 539 const result = await pool.query( 326 540 `INSERT INTO project.event(event_type,"date",start_time,end_time,status,wedding_id) … … 331 545 } catch (err) { 332 546 console.error(err.message); 547 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message }); 333 548 res.status(500).json({ error: 'Server error.' }); 334 549 } … … 339 554 const { event_type, date, start_time, end_time, status } = req.body; 340 555 try { 556 // fetch existing event to know wedding_id and current values 557 const evRes = await pool.query('SELECT * FROM project.event WHERE event_id=$1', [req.params.id]); 558 if (evRes.rows.length === 0) return res.status(404).json({ error: 'Event not found.' }); 559 const ev = evRes.rows[0]; 560 const weddingId = ev.wedding_id; 561 // Validate date matches wedding and no overlap (exclude this event) 562 await BookingService.validateDateMatchesWedding(weddingId, date); 563 await BookingService.validateNoOverlap(weddingId, date, start_time, end_time, { type: 'event', id: req.params.id }); 564 341 565 const result = await pool.query( 342 566 `UPDATE project.event SET event_type=$1,"date"=$2,start_time=$3,end_time=$4,status=$5 … … 348 572 } catch (err) { 349 573 console.error(err.message); 574 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message }); 350 575 res.status(500).json({ error: 'Server error.' }); 351 576 } … … 778 1003 // Check availability: no overlapping booking for same venue on same date 779 1004 try { 1005 // Business validations 1006 await BookingService.validateDateMatchesWedding(req.params.wid, date); 1007 await BookingService.validateNoOverlap(req.params.wid, date, start_time, end_time, null); 1008 780 1009 const conflict = await pool.query( 781 1010 `SELECT booking_id FROM project.venue_booking … … 795 1024 } catch (err) { 796 1025 console.error(err.message); 1026 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message }); 797 1027 res.status(500).json({ error: 'Server error.' }); 798 1028 } … … 848 1078 return res.status(400).json({ error: 'date, start_time, end_time and band_id are required.' }); 849 1079 try { 1080 // Business validations 1081 await BookingService.validateDateMatchesWedding(req.params.wid, date); 1082 await BookingService.validateNoOverlap(req.params.wid, date, start_time, end_time, null); 850 1083 const result = await pool.query( 851 1084 `INSERT INTO project.band_booking("date",start_time,end_time,status,band_id,wedding_id) … … 856 1089 } catch (err) { 857 1090 console.error(err.message); 1091 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message }); 858 1092 res.status(500).json({ error: 'Server error.' }); 859 1093 } … … 909 1143 return res.status(400).json({ error: 'date, start_time, end_time and photographer_id are required.' }); 910 1144 try { 1145 // Business validations 1146 await BookingService.validateDateMatchesWedding(req.params.wid, date); 1147 await BookingService.validateNoOverlap(req.params.wid, date, start_time, end_time, null); 911 1148 const result = await pool.query( 912 1149 `INSERT INTO project.photographer_booking("date",start_time,end_time,status,photographer_id,wedding_id) … … 917 1154 } catch (err) { 918 1155 console.error(err.message); 1156 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message }); 919 1157 res.status(500).json({ error: 'Server error.' }); 920 1158 } … … 977 1215 return res.status(400).json({ error: 'date, start_time, end_time and church_id are required.' }); 978 1216 try { 1217 // Business validations 1218 await BookingService.validateDateMatchesWedding(req.params.wid, date); 1219 await BookingService.validateNoOverlap(req.params.wid, date, start_time, end_time, null); 979 1220 const result = await pool.query( 980 1221 `INSERT INTO project.church_booking("date",start_time,end_time,status,church_id,wedding_id) … … 985 1226 } catch (err) { 986 1227 console.error(err.message); 1228 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message }); 987 1229 res.status(500).json({ error: 'Server error.' }); 988 1230 } … … 1038 1280 return res.status(400).json({ error: 'date, start_time, end_time and registrar_id are required.' }); 1039 1281 try { 1282 // Business validations 1283 await BookingService.validateDateMatchesWedding(req.params.wid, date); 1284 await BookingService.validateNoOverlap(req.params.wid, date, start_time, end_time, null); 1285 await BookingService.validateRegistrarLocation(req.params.wid, date, registrar_id); 1286 1040 1287 const result = await pool.query( 1041 1288 `INSERT INTO project.registrar_booking("date",start_time,end_time,status,registrar_id,wedding_id) … … 1046 1293 } catch (err) { 1047 1294 console.error(err.message); 1295 if (err instanceof ApiError) return res.status(err.status).json({ error: err.message }); 1048 1296 res.status(500).json({ error: 'Server error.' }); 1049 1297 }
Note:
See TracChangeset
for help on using the changeset viewer.
