feat(notifications): add admin notification acknowledgement on booking state changes

- Add AcknowledgePendingBookingNotification helper for acknowledging notifications
- ConfirmBookingHandler: acknowledge pending notification when booking confirmed
- Cancel handlers: acknowledge pending notification and only create cancelled_booking notification if booking was not in pending status
- Add user_notification_preferences table with email, sms, push enabled flags
- Update cancellation logic to check original status before creating notifications
This commit is contained in:
2026-02-17 21:38:50 +00:00
parent 105831eeb3
commit bbb273c192
4 changed files with 194 additions and 18 deletions
+63 -15
View File
@@ -2,6 +2,7 @@ package bookings
import (
"crussell/db"
"crussell/handlers/notifications"
"crussell/mw"
"database/sql"
"encoding/json"
@@ -1459,6 +1460,11 @@ func ConfirmBookingHandler(w http.ResponseWriter, r *http.Request) {
return
}
if err := notifications.AcknowledgePendingBookingNotification(tx, r.Context(), bookingID); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Update service overrides individually
if len(req.ServiceOverrides) > 0 {
// First, verify all service IDs belong to this booking
@@ -1574,6 +1580,19 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
}
defer tx.Rollback(r.Context())
// Get current status before updating
var originalStatus string
err = tx.QueryRow(r.Context(), "SELECT status FROM bookings WHERE id = $1 AND user_id = $2", bookingID, userID).Scan(&originalStatus)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Booking not found or access denied", http.StatusNotFound)
return
}
log.Printf("Failed to get booking status %s: %v", bookingID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Update booking status instead of deleting
query := `
UPDATE bookings
@@ -1592,18 +1611,26 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Create admin notification for cancelled booking
notificationQuery := `
INSERT INTO admin_notifications (reason, booking_id, user_id)
VALUES ($1, $2, $3)
`
_, err = tx.Exec(r.Context(), notificationQuery, "cancelled_booking", bookingID, userID)
if err != nil {
log.Printf("Failed to create admin notification for booking %s: %v", bookingID, err)
// Acknowledge pending notification if exists
if err := notifications.AcknowledgePendingBookingNotification(tx, r.Context(), bookingID); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Only notify on cancellation if booking was confirmed (not pending)
if originalStatus == "confirmed" {
notificationQuery := `
INSERT INTO admin_notifications (reason, booking_id, user_id)
VALUES ($1, $2, $3)
`
_, err = tx.Exec(r.Context(), notificationQuery, "cancelled_booking", bookingID, userID)
if err != nil {
log.Printf("Failed to create admin notification for booking %s: %v", bookingID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
}
if err := tx.Commit(r.Context()); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
@@ -1628,18 +1655,39 @@ func DeleteBookingHandler(w http.ResponseWriter, r *http.Request) {
}
defer tx.Rollback(r.Context())
// Create admin notification BEFORE deleting the booking
notificationQuery := `
INSERT INTO admin_notifications (reason, booking_id, user_id)
VALUES ($1, $2, $3)
`
_, err = tx.Exec(r.Context(), notificationQuery, "cancelled_booking", bookingID, userID)
// Get current status before deleting
var originalStatus string
err = tx.QueryRow(r.Context(), "SELECT status FROM bookings WHERE id = $1 AND user_id = $2", bookingID, userID).Scan(&originalStatus)
if err != nil {
log.Printf("Failed to create admin notification for booking %s: %v", bookingID, err)
if err == sql.ErrNoRows {
http.Error(w, "Booking not found or access denied", http.StatusNotFound)
return
}
log.Printf("Failed to get booking status %s: %v", bookingID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Acknowledge pending notification if exists
if err := notifications.AcknowledgePendingBookingNotification(tx, r.Context(), bookingID); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Only notify on cancellation if booking was not pending (e.g. confirmed, in_progress)
if originalStatus != "pending" {
notificationQuery := `
INSERT INTO admin_notifications (reason, booking_id, user_id)
VALUES ($1, $2, $3)
`
_, err = tx.Exec(r.Context(), notificationQuery, "cancelled_booking", bookingID, userID)
if err != nil {
log.Printf("Failed to create admin notification for booking %s: %v", bookingID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
}
// Now delete the booking
query := "DELETE FROM bookings WHERE id = $1 AND user_id = $2"
result, err := tx.Exec(r.Context(), query, bookingID, userID)
+98 -3
View File
@@ -2,6 +2,7 @@ package bookings
import (
"crussell/db"
"crussell/handlers/notifications"
"crussell/mw"
"database/sql"
"encoding/json"
@@ -13,7 +14,7 @@ import (
)
// UserCancelBookingHandler allows an authenticated user to cancel a booking they own.
// The update is performed in a single statement with appropriate conditions.
// The update is performed in a transaction with notification handling.
func UserCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
bookingID := chi.URLParam(r, "id")
@@ -23,7 +24,28 @@ func UserCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
return
}
res, err := db.DB.Exec(r.Context(), `
tx, err := db.DB.Begin(r.Context())
if err != nil {
log.Printf("Failed to start transaction: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
// Get current status before updating
var originalStatus string
err = tx.QueryRow(r.Context(), "SELECT status FROM bookings WHERE id = $1 AND user_id = $2", bookingID, userID).Scan(&originalStatus)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Booking not cancellable", http.StatusNotFound)
return
}
log.Printf("Failed to get booking status %s: %v", bookingID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
res, err := tx.Exec(r.Context(), `
UPDATE bookings
SET status = 'client_cancelled', updated_at = $1
WHERE id = $2 AND user_id = $3 AND status IN ('pending', 'confirmed', 'in_progress')
@@ -39,6 +61,32 @@ func UserCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Acknowledge pending notification if exists
if err := notifications.AcknowledgePendingBookingNotification(tx, r.Context(), bookingID); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Only notify on cancellation if booking was not pending (e.g. confirmed, in_progress)
if originalStatus != "pending" {
notificationQuery := `
INSERT INTO admin_notifications (reason, booking_id, user_id)
VALUES ($1, $2, $3)
`
_, err = tx.Exec(r.Context(), notificationQuery, "cancelled_booking", bookingID, userID)
if err != nil {
log.Printf("Failed to create admin notification for booking %s: %v", bookingID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
}
if err := tx.Commit(r.Context()); err != nil {
log.Printf("Failed to commit user cancel: %v", bookingID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
@@ -47,7 +95,28 @@ func UserCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
func AdminCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
bookingID := chi.URLParam(r, "id")
res, err := db.DB.Exec(r.Context(), `
tx, err := db.DB.Begin(r.Context())
if err != nil {
log.Printf("Failed to start transaction: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
defer tx.Rollback(r.Context())
// Get current status before updating
var originalStatus string
err = tx.QueryRow(r.Context(), "SELECT status FROM bookings WHERE id = $1", bookingID).Scan(&originalStatus)
if err != nil {
if err == sql.ErrNoRows {
http.Error(w, "Booking not cancellable", http.StatusNotFound)
return
}
log.Printf("Failed to get booking status %s: %v", bookingID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
res, err := tx.Exec(r.Context(), `
UPDATE bookings
SET status = 'we_cancelled', updated_at = $1
WHERE id = $2 AND status IN ('pending', 'confirmed', 'in_progress')
@@ -63,6 +132,32 @@ func AdminCancelBookingHandler(w http.ResponseWriter, r *http.Request) {
return
}
// Acknowledge pending notification if exists
if err := notifications.AcknowledgePendingBookingNotification(tx, r.Context(), bookingID); err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
// Only notify on cancellation if booking was not pending (e.g. confirmed, in_progress)
if originalStatus != "pending" {
notificationQuery := `
INSERT INTO admin_notifications (reason, booking_id, user_id)
SELECT 'cancelled_booking', $1, user_id FROM bookings WHERE id = $1
`
_, err = tx.Exec(r.Context(), notificationQuery, bookingID)
if err != nil {
log.Printf("Failed to create admin notification for booking %s: %v", bookingID, err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
}
if err := tx.Commit(r.Context()); err != nil {
log.Printf("Failed to commit admin cancel: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusNoContent)
}
@@ -1,6 +1,7 @@
package notifications
import (
"context"
"crussell/db"
"database/sql"
"encoding/json"
@@ -11,6 +12,7 @@ import (
"time"
"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5/pgconn"
)
// Structs returned in JSON
@@ -173,3 +175,19 @@ func AcknowledgeNotification(w http.ResponseWriter, r *http.Request) {
"status": "ok",
})
}
func AcknowledgePendingBookingNotification(tx interface{}, ctx context.Context, bookingID string) error {
query := `
UPDATE admin_notifications
SET acknowledged_at = NOW()
WHERE booking_id = $1 AND reason = 'pending_booking' AND acknowledged_at IS NULL
`
_, err := tx.(interface {
Exec(ctx context.Context, sql string, arguments ...interface{}) (pgconn.CommandTag, error)
}).Exec(ctx, query, bookingID)
if err != nil {
log.Printf("Failed to acknowledge pending booking notification for %s: %v", bookingID, err)
return err
}
return nil
}