project-startup 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
const db = require("../config/db");
|
|
2
|
+
|
|
3
|
+
// GET /api/bookings
|
|
4
|
+
// Manager → all bookings with customer + bus details
|
|
5
|
+
// Customer → only their own bookings
|
|
6
|
+
exports.getBookings = async (req, res) => {
|
|
7
|
+
if (req.user.role === "manager") {
|
|
8
|
+
const [bookings] = await db.query(
|
|
9
|
+
`SELECT
|
|
10
|
+
bk.id,
|
|
11
|
+
bk.seats,
|
|
12
|
+
bk.total_rwf,
|
|
13
|
+
bk.booked_at,
|
|
14
|
+
u.name AS customer_name,
|
|
15
|
+
u.email AS customer_email,
|
|
16
|
+
b.plate_number,
|
|
17
|
+
b.destination,
|
|
18
|
+
b.departure_time,
|
|
19
|
+
b.price_rwf
|
|
20
|
+
FROM bookings bk
|
|
21
|
+
JOIN users u ON u.id = bk.customer_id
|
|
22
|
+
JOIN buses b ON b.id = bk.bus_id
|
|
23
|
+
ORDER BY bk.booked_at DESC`
|
|
24
|
+
);
|
|
25
|
+
return res.json(bookings);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Customer: only their rows
|
|
29
|
+
const [bookings] = await db.query(
|
|
30
|
+
`SELECT
|
|
31
|
+
bk.id,
|
|
32
|
+
bk.seats,
|
|
33
|
+
bk.total_rwf,
|
|
34
|
+
bk.booked_at,
|
|
35
|
+
b.plate_number,
|
|
36
|
+
b.destination,
|
|
37
|
+
b.departure_time,
|
|
38
|
+
b.price_rwf
|
|
39
|
+
FROM bookings bk
|
|
40
|
+
JOIN buses b ON b.id = bk.bus_id
|
|
41
|
+
WHERE bk.customer_id = ?
|
|
42
|
+
ORDER BY bk.booked_at DESC`,
|
|
43
|
+
[req.user.id]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
res.json(bookings);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// POST /api/bookings (customer only)
|
|
50
|
+
exports.createBooking = async (req, res) => {
|
|
51
|
+
const { busId, seats } = req.body;
|
|
52
|
+
|
|
53
|
+
if (!busId || !seats || seats < 1) {
|
|
54
|
+
return res.status(400).json({ error: "busId and seats (≥ 1) are required." });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Critical section: read and update in one atomic step ───────────────
|
|
58
|
+
// We use a transaction so two customers can't both grab the last seat.
|
|
59
|
+
const conn = await db.getConnection();
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
await conn.beginTransaction();
|
|
63
|
+
|
|
64
|
+
// Lock this bus row for the duration of the transaction (FOR UPDATE)
|
|
65
|
+
// This prevents another request from reading stale available_seats
|
|
66
|
+
const [rows] = await conn.query(
|
|
67
|
+
"SELECT * FROM buses WHERE id = ? FOR UPDATE",
|
|
68
|
+
[busId]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (rows.length === 0) {
|
|
72
|
+
await conn.rollback();
|
|
73
|
+
return res.status(404).json({ error: "Bus not found." });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const bus = rows[0];
|
|
77
|
+
|
|
78
|
+
if (new Date(bus.departure_time) <= new Date()) {
|
|
79
|
+
await conn.rollback();
|
|
80
|
+
return res.status(400).json({ error: "This bus has already departed." });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (seats > bus.available_seats) {
|
|
84
|
+
await conn.rollback();
|
|
85
|
+
return res.status(400).json({
|
|
86
|
+
error: `Only ${bus.available_seats} seat(s) available.`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Decrement available_seats
|
|
91
|
+
await conn.query(
|
|
92
|
+
"UPDATE buses SET available_seats = available_seats - ? WHERE id = ?",
|
|
93
|
+
[seats, busId]
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// Insert the booking record
|
|
97
|
+
const [result] = await conn.query(
|
|
98
|
+
`INSERT INTO bookings (bus_id, customer_id, seats, total_rwf)
|
|
99
|
+
VALUES (?, ?, ?, ?)`,
|
|
100
|
+
[busId, req.user.id, seats, seats * bus.price_rwf]
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
await conn.commit();
|
|
104
|
+
|
|
105
|
+
// Return the new booking with bus details attached
|
|
106
|
+
const [booking] = await conn.query(
|
|
107
|
+
`SELECT
|
|
108
|
+
bk.id,
|
|
109
|
+
bk.seats,
|
|
110
|
+
bk.total_rwf,
|
|
111
|
+
bk.booked_at,
|
|
112
|
+
b.plate_number,
|
|
113
|
+
b.destination,
|
|
114
|
+
b.departure_time
|
|
115
|
+
FROM bookings bk
|
|
116
|
+
JOIN buses b ON b.id = bk.bus_id
|
|
117
|
+
WHERE bk.id = ?`,
|
|
118
|
+
[result.insertId]
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
res.status(201).json(booking[0]);
|
|
122
|
+
|
|
123
|
+
} catch (err) {
|
|
124
|
+
await conn.rollback();
|
|
125
|
+
throw err;
|
|
126
|
+
} finally {
|
|
127
|
+
conn.release(); // always return connection to pool
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// DELETE /api/bookings/:id (customer only — own bookings)
|
|
132
|
+
exports.cancelBooking = async (req, res) => {
|
|
133
|
+
const conn = await db.getConnection();
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
await conn.beginTransaction();
|
|
137
|
+
|
|
138
|
+
// Find the booking AND confirm it belongs to this customer
|
|
139
|
+
const [rows] = await conn.query(
|
|
140
|
+
"SELECT * FROM bookings WHERE id = ? AND customer_id = ?",
|
|
141
|
+
[req.params.id, req.user.id]
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
if (rows.length === 0) {
|
|
145
|
+
await conn.rollback();
|
|
146
|
+
return res.status(404).json({ error: "Booking not found." });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const booking = rows[0];
|
|
150
|
+
|
|
151
|
+
// Restore seats only if bus hasn't departed yet
|
|
152
|
+
const [busRows] = await conn.query(
|
|
153
|
+
"SELECT departure_time FROM buses WHERE id = ?",
|
|
154
|
+
[booking.bus_id]
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (busRows.length > 0 && new Date(busRows[0].departure_time) > new Date()) {
|
|
158
|
+
await conn.query(
|
|
159
|
+
"UPDATE buses SET available_seats = available_seats + ? WHERE id = ?",
|
|
160
|
+
[booking.seats, booking.bus_id]
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
await conn.query("DELETE FROM bookings WHERE id = ?", [booking.id]);
|
|
165
|
+
|
|
166
|
+
await conn.commit();
|
|
167
|
+
|
|
168
|
+
res.json({ message: "Booking cancelled. Seats have been restored." });
|
|
169
|
+
|
|
170
|
+
} catch (err) {
|
|
171
|
+
await conn.rollback();
|
|
172
|
+
throw err;
|
|
173
|
+
} finally {
|
|
174
|
+
conn.release();
|
|
175
|
+
}
|
|
176
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const db = require("../config/db");
|
|
2
|
+
|
|
3
|
+
// GET /api/buses
|
|
4
|
+
exports.getAllBuses = async (req, res) => {
|
|
5
|
+
const [buses] = await db.query(
|
|
6
|
+
"SELECT * FROM buses ORDER BY departure_time ASC"
|
|
7
|
+
);
|
|
8
|
+
res.json(buses);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// GET /api/buses/:id
|
|
12
|
+
exports.getBusById = async (req, res) => {
|
|
13
|
+
const [rows] = await db.query(
|
|
14
|
+
"SELECT * FROM buses WHERE id = ?",
|
|
15
|
+
[req.params.id]
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
if (rows.length === 0) {
|
|
19
|
+
return res.status(404).json({ error: "Bus not found." });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
res.json(rows[0]);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// POST /api/buses (manager only)
|
|
26
|
+
exports.createBus = async (req, res) => {
|
|
27
|
+
const { plateNumber, destination, maxSeats, departureTime, priceRwf } = req.body;
|
|
28
|
+
|
|
29
|
+
if (!plateNumber || !destination || !maxSeats || !departureTime || !priceRwf) {
|
|
30
|
+
return res.status(400).json({ error: "All fields are required." });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (new Date(departureTime) <= new Date()) {
|
|
34
|
+
return res.status(400).json({ error: "Departure time must be in the future." });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Check for duplicate plate number
|
|
38
|
+
const [existing] = await db.query(
|
|
39
|
+
"SELECT id FROM buses WHERE plate_number = ?",
|
|
40
|
+
[plateNumber]
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (existing.length > 0) {
|
|
44
|
+
return res.status(400).json({ error: "Plate number already exists." });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const [result] = await db.query(
|
|
48
|
+
`INSERT INTO buses
|
|
49
|
+
(plate_number, destination, max_seats, available_seats, departure_time, price_rwf)
|
|
50
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
51
|
+
[plateNumber, destination, maxSeats, maxSeats, departureTime, priceRwf]
|
|
52
|
+
// ^ available_seats starts equal to max_seats
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// Fetch and return the newly created row
|
|
56
|
+
const [rows] = await db.query(
|
|
57
|
+
"SELECT * FROM buses WHERE id = ?",
|
|
58
|
+
[result.insertId]
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
res.status(201).json(rows[0]);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// PUT /api/buses/:id (manager only)
|
|
65
|
+
exports.updateBus = async (req, res) => {
|
|
66
|
+
const { plateNumber, destination, maxSeats, departureTime, priceRwf } = req.body;
|
|
67
|
+
|
|
68
|
+
// Fetch current state first so we can calculate available_seats correctly
|
|
69
|
+
const [rows] = await db.query(
|
|
70
|
+
"SELECT * FROM buses WHERE id = ?",
|
|
71
|
+
[req.params.id]
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (rows.length === 0) {
|
|
75
|
+
return res.status(404).json({ error: "Bus not found." });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const bus = rows[0];
|
|
79
|
+
|
|
80
|
+
// If maxSeats is being changed, recalculate available_seats:
|
|
81
|
+
// available = newMax - (oldMax - oldAvailable) ← keeps already-booked seats
|
|
82
|
+
let newAvailable = bus.available_seats;
|
|
83
|
+
|
|
84
|
+
if (maxSeats) {
|
|
85
|
+
const booked = bus.max_seats - bus.available_seats;
|
|
86
|
+
if (parseInt(maxSeats) < booked) {
|
|
87
|
+
return res.status(400).json({
|
|
88
|
+
error: `Cannot reduce max seats below already booked count (${booked}).`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
newAvailable = parseInt(maxSeats) - booked;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await db.query(
|
|
95
|
+
`UPDATE buses SET
|
|
96
|
+
plate_number = COALESCE(?, plate_number),
|
|
97
|
+
destination = COALESCE(?, destination),
|
|
98
|
+
max_seats = COALESCE(?, max_seats),
|
|
99
|
+
available_seats = ?,
|
|
100
|
+
departure_time = COALESCE(?, departure_time),
|
|
101
|
+
price_rwf = COALESCE(?, price_rwf)
|
|
102
|
+
WHERE id = ?`,
|
|
103
|
+
[
|
|
104
|
+
plateNumber || null,
|
|
105
|
+
destination || null,
|
|
106
|
+
maxSeats || null,
|
|
107
|
+
newAvailable,
|
|
108
|
+
departureTime|| null,
|
|
109
|
+
priceRwf || null,
|
|
110
|
+
req.params.id,
|
|
111
|
+
]
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Return updated row
|
|
115
|
+
const [updated] = await db.query("SELECT * FROM buses WHERE id = ?", [req.params.id]);
|
|
116
|
+
res.json(updated[0]);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// DELETE /api/buses/:id (manager only)
|
|
120
|
+
exports.deleteBus = async (req, res) => {
|
|
121
|
+
// Check if any bookings exist for this bus
|
|
122
|
+
const [bookings] = await db.query(
|
|
123
|
+
"SELECT id FROM bookings WHERE bus_id = ?",
|
|
124
|
+
[req.params.id]
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
if (bookings.length > 0) {
|
|
128
|
+
return res.status(400).json({
|
|
129
|
+
error: "Cannot delete a bus that has existing bookings.",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const [result] = await db.query(
|
|
134
|
+
"DELETE FROM buses WHERE id = ?",
|
|
135
|
+
[req.params.id]
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (result.affectedRows === 0) {
|
|
139
|
+
return res.status(404).json({ error: "Bus not found." });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
res.json({ message: "Bus deleted." });
|
|
143
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
-- Run this file once to set up your database:
|
|
2
|
+
-- mysql -u root -p < schema.sql
|
|
3
|
+
|
|
4
|
+
CREATE DATABASE IF NOT EXISTS fleet_db;
|
|
5
|
+
USE fleet_db;
|
|
6
|
+
|
|
7
|
+
-- ─── Users ────────────────────────────────────────────────────────────────────
|
|
8
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
9
|
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
10
|
+
name VARCHAR(100) NOT NULL,
|
|
11
|
+
email VARCHAR(150) UNIQUE NOT NULL,
|
|
12
|
+
password VARCHAR(255) NOT NULL,
|
|
13
|
+
role ENUM('manager', 'customer') NOT NULL,
|
|
14
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
-- ─── Buses ────────────────────────────────────────────────────────────────────
|
|
18
|
+
CREATE TABLE IF NOT EXISTS buses (
|
|
19
|
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
20
|
+
plate_number VARCHAR(20) UNIQUE NOT NULL,
|
|
21
|
+
destination VARCHAR(100) NOT NULL,
|
|
22
|
+
max_seats INT NOT NULL,
|
|
23
|
+
available_seats INT NOT NULL,
|
|
24
|
+
departure_time DATETIME NOT NULL,
|
|
25
|
+
price_rwf INT NOT NULL,
|
|
26
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
-- ─── Bookings ─────────────────────────────────────────────────────────────────
|
|
30
|
+
CREATE TABLE IF NOT EXISTS bookings (
|
|
31
|
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
32
|
+
bus_id INT NOT NULL,
|
|
33
|
+
customer_id INT NOT NULL,
|
|
34
|
+
seats INT NOT NULL,
|
|
35
|
+
total_rwf INT NOT NULL,
|
|
36
|
+
booked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
37
|
+
FOREIGN KEY (bus_id) REFERENCES buses(id) ON DELETE CASCADE,
|
|
38
|
+
FOREIGN KEY (customer_id) REFERENCES users(id) ON DELETE CASCADE
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
-- ─── Sessions ─────────────────────────────────────────────────────────────────
|
|
42
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
43
|
+
token VARCHAR(255) PRIMARY KEY,
|
|
44
|
+
user_id INT NOT NULL,
|
|
45
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
46
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
47
|
+
);
|