Skip to content

Commit c0adc5a

Browse files
feat: added cancel enums to booking
1 parent 5983dd5 commit c0adc5a

File tree

12 files changed

+537
-27
lines changed

12 files changed

+537
-27
lines changed

cmd/api/api.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ func (app *application) mount() http.Handler {
123123
r.Get("/is-venue-owner", app.isVenueOwnerHandler)
124124
r.Post("/", app.createVenueHandler)
125125
r.Post("/{venueID}/reviews", app.createVenueReviewHandler)
126+
r.Post("/{venueID}/cancel-bookings/{bookingID}", app.cancelBookingHandler)
126127
r.Post("/{venueID}/bookings", app.bookVenueHandler)
127128
r.Get("/{venueID}/reviews", app.getVenueReviewsHandler)
128129
r.Post("/{venueID}/favorite", app.addFavoriteHandler) // Add favorite
@@ -135,6 +136,7 @@ func (app *application) mount() http.Handler {
135136
r.Get("/pricing", app.getVenuePricing)
136137
r.Delete("/", app.deleteVenueHandler)
137138
r.Get("/pending-bookings", app.getPendingBookingsHandler)
139+
r.Get("/canceled-bookings", app.getCanceledBookingsHandler)
138140
r.Get("/venue-info", app.getVenueInfoHandler)
139141
r.Get("/scheduled-bookings", app.getScheduledBookingsHandler)
140142
r.Post("/pending-bookings/{bookingID}/accept", app.acceptBookingHandler)
@@ -219,7 +221,7 @@ func (app *application) mount() http.Handler {
219221
return r
220222
}
221223

222-
func (app *application) run(mux http.Handler) error {
224+
func (app *application) run(mux http.Handler, cancel context.CancelFunc) error {
223225
// Docs
224226
docs.SwaggerInfo.Version = version
225227
docs.SwaggerInfo.Host = app.config.apiURL
@@ -243,21 +245,21 @@ func (app *application) run(mux http.Handler) error {
243245

244246
go func() {
245247
quit := make(chan os.Signal, 1)
246-
247248
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
249+
248250
s := <-quit
251+
app.logger.Infow("signal caught", "signal", s.String())
249252

250-
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
251-
defer cancel()
253+
cancel()
252254

253-
app.logger.Infow("signal caught", "signal", s.String())
255+
ctx, cancelTimeout := context.WithTimeout(context.Background(), 5*time.Second)
256+
defer cancelTimeout()
254257

255258
if err := srv.Shutdown(ctx); err != nil {
256259
shutdown <- err
257260
}
258261

259262
close(shutdown)
260-
261263
}()
262264

263265
app.logger.Infow("server has started", "addr", app.config.addr, "env", app.config.env)

cmd/api/background.go

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,39 @@
11
package main
22

33
import (
4+
"context"
45
"time"
56
)
67

7-
func (app *application) markCompletedGamesEvery30Mins() {
8+
func (app *application) runMarkCompletedGames() {
9+
if err := app.store.Games.MarkCompletedGames(); err != nil {
10+
app.logger.Errorf("Error marking games as completed: %v", err)
11+
} else {
12+
app.logger.Infof("Successfully marked games as completed at %s", time.Now().UTC().Format(time.RFC3339))
13+
}
14+
}
15+
16+
func (app *application) markCompletedGamesEvery30Mins(ctx context.Context) {
817
go func() {
18+
19+
defer func() {
20+
if r := recover(); r != nil {
21+
app.logger.Errorf("Recovered from panic in markCompletedGamesEvery30Mins: %v", r)
22+
}
23+
}()
924
ticker := time.NewTicker(30 * time.Minute)
1025
defer ticker.Stop()
1126

1227
// Run once immediately
13-
err := app.store.Games.MarkCompletedGames()
14-
if err != nil {
15-
app.logger.Errorf("Error marking games as completed: %v", err)
16-
} else {
17-
app.logger.Infof("Successfully marked games as completed at %s", time.Now().Format(time.RFC1123))
18-
}
28+
app.runMarkCompletedGames()
1929

20-
// Then run every 30 minutes
21-
for range ticker.C {
22-
err := app.store.Games.MarkCompletedGames()
23-
if err != nil {
24-
app.logger.Errorf("Error marking games as completed: %v", err)
25-
} else {
26-
app.logger.Infof("Successfully marked games as completed at %s", time.Now().Format(time.RFC1123))
30+
for {
31+
select {
32+
case <-ctx.Done():
33+
app.logger.Info("Stopped markCompletedGamesEvery30Mins due to context cancellation")
34+
return
35+
case <-ticker.C:
36+
app.runMarkCompletedGames()
2737
}
2838
}
2939
}()

cmd/api/booking.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,57 @@ func (app *application) getScheduledBookingsHandler(w http.ResponseWriter, r *ht
712712
app.jsonResponse(w, http.StatusOK, bookings)
713713
}
714714

715+
// getCanceledBookingsHandler godoc
716+
//
717+
// @Summary List Canceled booking requests for a venue
718+
// @Description Returns all bookings with status="canceled" for a given venue and date.
719+
// @Tags Venue-Owner
720+
// @Accept json
721+
// @Produce json
722+
// @Param venueID path int true "Venue ID"
723+
// @Param date query string true "Date in YYYY-MM-DD format"
724+
// @Success 200 {array} store.ScheduledBooking "Scheduled bookings"
725+
// @Failure 400 {object} error "Bad Request"
726+
// @Failure 500 {object} error "Internal Server Error"
727+
// @Security ApiKeyAuth
728+
// @Router /venues/{venueID}/canceled-bookings [get]
729+
func (app *application) getCanceledBookingsHandler(w http.ResponseWriter, r *http.Request) {
730+
// 1) parse venueID
731+
vid, err := strconv.ParseInt(chi.URLParam(r, "venueID"), 10, 64)
732+
if err != nil {
733+
app.badRequestResponse(w, r, fmt.Errorf("invalid venueID: %w", err))
734+
return
735+
}
736+
737+
// 2) parse date query
738+
dateStr := r.URL.Query().Get("date")
739+
if dateStr == "" {
740+
app.badRequestResponse(w, r, errors.New("missing date"))
741+
return
742+
}
743+
744+
loc, _ := time.LoadLocation("Asia/Kathmandu")
745+
746+
// Parse date string in Kathmandu local time
747+
date, err := time.ParseInLocation("2006-01-02", dateStr, loc)
748+
if err != nil {
749+
app.badRequestResponse(w, r, fmt.Errorf("invalid date format: %w", err))
750+
return
751+
}
752+
753+
//date will be Parsed time is: 2025-06-29 00:00:00 +0545 +0545
754+
755+
// 3) fetch from store
756+
bookings, err := app.store.Bookings.GetCanceledBookingsForVenueDate(r.Context(), vid, date)
757+
if err != nil {
758+
app.internalServerError(w, r, err)
759+
return
760+
}
761+
762+
// 4) respond JSON
763+
app.jsonResponse(w, http.StatusOK, bookings)
764+
}
765+
715766
// acceptBookingHandler godoc
716767
//
717768
// @Summary Accept a pending booking request
@@ -797,6 +848,65 @@ func (app *application) rejectBookingHandler(w http.ResponseWriter, r *http.Requ
797848
w.WriteHeader(http.StatusNoContent)
798849
}
799850

851+
// cancelBookingHandler godoc
852+
//
853+
// @Summary Cancel a pending booking request or confirmed booking
854+
// @Description Marks the booking with status="pending or confirmed" as "canceled".
855+
// @Tags Venue
856+
// @Accept json
857+
// @Produce json
858+
// @Param venueID path int true "Venue ID"
859+
// @Param bookingID path int true "Booking ID"
860+
// @Success 204
861+
// @Failure 400 {object} error "Bad Request"
862+
// @Failure 404 {object} error "Not Found"
863+
// @Failure 500 {object} error "Internal Server Error"
864+
// @Security ApiKeyAuth
865+
// @Router /venues/{venueID}/cancel-bookings/{bookingID} [post]
866+
func (app *application) cancelBookingHandler(w http.ResponseWriter, r *http.Request) {
867+
vid, err := strconv.ParseInt(chi.URLParam(r, "venueID"), 10, 64)
868+
if err != nil {
869+
app.badRequestResponse(w, r, fmt.Errorf("invalid venueID: %w", err))
870+
return
871+
}
872+
bid, err := strconv.ParseInt(chi.URLParam(r, "bookingID"), 10, 64)
873+
if err != nil {
874+
app.badRequestResponse(w, r, fmt.Errorf("invalid bookingID: %w", err))
875+
return
876+
}
877+
878+
// 🔐 Step 1: Extract userID from context (set by your auth middleware)
879+
authUser := getUserFromContext(r) // assuming this returns a struct with ID
880+
if authUser == nil {
881+
app.unauthorizedErrorResponse(w, r, errors.New("check Bearer token"))
882+
return
883+
}
884+
885+
// 🔍 Step 2: Check ownership
886+
ownerID, err := app.store.Bookings.GetBookingOwner(r.Context(), vid, bid)
887+
if err != nil {
888+
if err == sql.ErrNoRows {
889+
app.notFoundResponse(w, r, errors.New("booking not found"))
890+
} else {
891+
app.internalServerError(w, r, err)
892+
}
893+
return
894+
}
895+
896+
if ownerID != authUser.ID {
897+
app.forbiddenResponse(w, r)
898+
return
899+
}
900+
901+
// ✅ Step 3: Cancel booking
902+
if err := app.store.Bookings.CancelBooking(r.Context(), vid, bid); err != nil {
903+
app.internalServerError(w, r, err)
904+
return
905+
}
906+
907+
w.WriteHeader(http.StatusNoContent)
908+
}
909+
800910
// getBookingsByUserHandler godoc
801911
//
802912
// @Summary List all bookings for a user

cmd/api/games.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,9 @@ func (app *application) createGameHandler(w http.ResponseWriter, r *http.Request
5454
app.badRequestResponse(w, r, err)
5555
return
5656
}
57-
//TODO: delete later
58-
fmt.Printf("validated payload is %v:", payload)
5957

6058
// 2. Get the authenticated user
6159
user := getUserFromContext(r)
62-
//TODO: delete later
63-
fmt.Printf("the user to create game is %v", user.FirstName)
6460

6561
// 4. Create the game
6662
game := &store.Game{

cmd/api/main.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"context"
45
"expvar"
56
"fmt"
67
"khel/internal/auth"
@@ -97,7 +98,7 @@ var version = "1.2.0"
9798
func main() {
9899
err := godotenv.Load()
99100
if err != nil {
100-
log.Fatalf("Error loading .env file: %v", err)
101+
log.Fatalf("Error loading .env file via godotenv.load: %v", err)
101102
}
102103
// Retrieve and convert maxOpenConns
103104
maxOpenConnsStr := os.Getenv("DB_MAX_OPEN_CONNS")
@@ -213,9 +214,12 @@ func main() {
213214
return runtime.NumGoroutine()
214215
}))
215216

216-
app.markCompletedGamesEvery30Mins()
217+
ctx, cancel := context.WithCancel(context.Background())
218+
defer cancel()
219+
220+
app.markCompletedGamesEvery30Mins(ctx)
217221

218222
mux := app.mount()
219223

220-
logger.Fatal(app.run(mux))
224+
logger.Fatal(app.run(mux, cancel))
221225
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
-- 1. Delete rows that use 'canceled' (or handle them another way)
2+
DELETE FROM bookings WHERE status = 'canceled';
3+
4+
-- 2. Rename current enum
5+
ALTER TYPE booking_status RENAME TO booking_status_old;
6+
7+
-- 3. Recreate enum without 'canceled'
8+
CREATE TYPE booking_status AS ENUM ('confirmed', 'pending', 'rejected', 'done');
9+
10+
-- 4. Alter column to use new enum
11+
ALTER TABLE bookings
12+
ALTER COLUMN status TYPE booking_status
13+
USING status::text::booking_status;
14+
15+
-- 5. Drop old enum type
16+
DROP TYPE booking_status_old;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- Add new value to the existing enum
2+
ALTER TYPE booking_status ADD VALUE IF NOT EXISTS 'canceled';

0 commit comments

Comments
 (0)