Skip to content

Commit 94f9679

Browse files
feat: added venue owner manual booking
1 parent 8cd5ffe commit 94f9679

File tree

9 files changed

+496
-21
lines changed

9 files changed

+496
-21
lines changed

cmd/api/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ func (app *application) mount() http.Handler {
131131
// Routes that require venue ownership
132132
r.Route("/{venueID}", func(r chi.Router) {
133133
r.Use(app.IsOwnerMiddleware)
134+
r.Post("/bookings/manual", app.createManualBookingHandler)
134135
r.Get("/pricing", app.getVenuePricing)
135136
r.Delete("/", app.deleteVenueHandler)
136137
r.Get("/pending-bookings", app.getPendingBookingsHandler)

cmd/api/booking.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,121 @@ func intervalsOverlap(a, b store.Interval) bool {
308308
return a.Start.Before(b.End) && b.Start.Before(a.End)
309309
}
310310

311+
type ManualBookingPayload struct {
312+
StartTime time.Time `json:"start_time" validate:"required"`
313+
EndTime time.Time `json:"end_time" validate:"required"`
314+
Price int `json:"price" validate:"required,gt=0"`
315+
Email string `json:"customer_email" validate:"omitempty,email"`
316+
CustomerName string `json:"customer_name" validate:"omitempty,max=100"`
317+
CustomerPhone string `json:"customer_number" validate:"omitempty,nepaliphone"`
318+
Note string `json:"note" validate:"omitempty,max=255"`
319+
}
320+
321+
// CreateManualBooking godoc
322+
//
323+
// @Summary Manually create a confirmed booking
324+
// @Description Venue owners can create a confirmed booking manually by specifying start/end time, price, and optional customer details.
325+
// @Tags Venue-Owner
326+
// @Accept json
327+
// @Produce json
328+
// @Param venueID path int true "Venue ID"
329+
// @Param payload body ManualBookingPayload true "Manual booking payload"
330+
// @Success 201 {object} store.Booking "Booking created successfully"
331+
// @Failure 400 {object} error "Bad Request: Invalid input or validation failed"
332+
// @Failure 401 {object} error "Unauthorized: Missing or invalid credentials"
333+
// @Failure 409 {object} error "Conflict: Time slot is already booked"
334+
// @Failure 500 {object} error "Internal Server Error: Could not create booking"
335+
// @Security ApiKeyAuth
336+
// @Router /venues/{venueID}/bookings/manual [post]
337+
func (app *application) createManualBookingHandler(w http.ResponseWriter, r *http.Request) {
338+
venueIDStr := chi.URLParam(r, "venueID")
339+
venueID, err := strconv.ParseInt(venueIDStr, 10, 64)
340+
if err != nil {
341+
http.Error(w, "Invalid venue ID", http.StatusBadRequest)
342+
return
343+
}
344+
var payload ManualBookingPayload
345+
if err := readJSON(w, r, &payload); err != nil {
346+
app.badRequestResponse(w, r, err)
347+
return
348+
}
349+
350+
fmt.Printf("⏰Start_time manualBooking: %s\n", payload.StartTime)
351+
fmt.Printf("⏰End_time manualBooking: %s\n", payload.EndTime)
352+
353+
if err := Validate.Struct(payload); err != nil {
354+
app.badRequestResponse(w, r, err)
355+
}
356+
357+
//sample data
358+
//frontend send like start_time 🎯 : 2025-06-29T11:00:00+05:45
359+
//end_time 🎯 : 2025-06-29T12:00:00+05:45
360+
//serverlog start_time 🎯: 2025-07-02 08:00:00 +0545 +0545
361+
//serverlog end_time 🎯: 2025-07-02 09:00:00 +0545 +0545
362+
363+
// Check for overlapping bookings.
364+
bookings, err := app.store.Bookings.GetBookingsForDate(r.Context(), venueID, payload.StartTime)
365+
if err != nil {
366+
http.Error(w, "Error checking bookings", http.StatusInternalServerError)
367+
return
368+
}
369+
requestedInterval := store.Interval{Start: payload.StartTime, End: payload.EndTime}
370+
for _, b := range bookings {
371+
if intervalsOverlap(requestedInterval, b) {
372+
http.Error(w, "Time slot is already booked", http.StatusConflict)
373+
return
374+
}
375+
}
376+
377+
user := getUserFromContext(r)
378+
if user == nil {
379+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
380+
return
381+
}
382+
383+
// Determine userID
384+
var bookingUserID int64 = user.ID
385+
if payload.Email != "" {
386+
targetUser, err := app.store.Users.GetByEmail(r.Context(), payload.Email)
387+
if err == nil && targetUser != nil {
388+
bookingUserID = targetUser.ID
389+
fmt.Printf("Target user name is: %s targetUserID 🎯: %d\n ", targetUser.FirstName, targetUser.ID)
390+
} else {
391+
log.Printf("Email %s not found, using owner ID", payload.Email)
392+
}
393+
}
394+
//Trim empty strings before setting pointer fields (to avoid storing "" instead of NULL)
395+
var namePtr, phonePtr, notePtr *string
396+
if strings.TrimSpace(payload.CustomerName) != "" {
397+
namePtr = &payload.CustomerName
398+
}
399+
if strings.TrimSpace(payload.CustomerPhone) != "" {
400+
phonePtr = &payload.CustomerPhone
401+
}
402+
if strings.TrimSpace(payload.Note) != "" {
403+
notePtr = &payload.Note
404+
}
405+
406+
booking := &store.Booking{
407+
VenueID: venueID,
408+
UserID: bookingUserID,
409+
StartTime: payload.StartTime,
410+
EndTime: payload.EndTime,
411+
TotalPrice: payload.Price,
412+
Status: "confirmed",
413+
CustomerName: namePtr,
414+
CustomerPhone: phonePtr,
415+
Note: notePtr,
416+
}
417+
418+
if err := app.store.Bookings.CreateBooking(r.Context(), booking); err != nil {
419+
app.internalServerError(w, r, err)
420+
return
421+
}
422+
423+
app.jsonResponse(w, http.StatusCreated, booking)
424+
}
425+
311426
// GetVenuePricing godoc
312427
//
313428
// @Summary Retrieve pricing slots for a venue (optionally filtered by day)

cmd/api/json.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"encoding/json"
55
"net/http"
6+
"regexp"
67

78
"github.com/go-playground/validator/v10"
89
)
@@ -16,6 +17,14 @@ var Validate *validator.Validate
1617

1718
func init() {
1819
Validate = validator.New(validator.WithRequiredStructEnabled())
20+
21+
// Register custom validation for Nepali phone numbers
22+
Validate.RegisterValidation("nepaliphone", func(fl validator.FieldLevel) bool {
23+
phone := fl.Field().String()
24+
// Matches 98[4-9] followed by 7 digits (e.g., 9841234567)
25+
matched, _ := regexp.MatchString(`^98[4-9][0-9]{7}$`, phone)
26+
return matched
27+
})
1928
}
2029

2130
func writeJSON(w http.ResponseWriter, status int, data any) error {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
ALTER TABLE bookings
2+
DROP COLUMN customer_name,
3+
DROP COLUMN note,
4+
DROP COLUMN customer_phone;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
ALTER TABLE bookings
2+
ADD COLUMN customer_name VARCHAR(100),
3+
ADD COLUMN note TEXT CHECK (char_length(note) <= 255),
4+
ADD COLUMN customer_phone VARCHAR(15)
5+
CHECK (
6+
customer_phone ~ '^0[1-9][0-9]{7}$' OR
7+
customer_phone ~ '^98[4-9][0-9]{7}$'
8+
);

docs/docs.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2473,6 +2473,68 @@ const docTemplate = `{
24732473
}
24742474
}
24752475
},
2476+
"/venues/{venueID}/bookings/manual": {
2477+
"post": {
2478+
"security": [
2479+
{
2480+
"ApiKeyAuth": []
2481+
}
2482+
],
2483+
"description": "Venue owners can create a confirmed booking manually by specifying start/end time, price, and optional customer details.",
2484+
"consumes": [
2485+
"application/json"
2486+
],
2487+
"produces": [
2488+
"application/json"
2489+
],
2490+
"tags": [
2491+
"Venue-Owner"
2492+
],
2493+
"summary": "Manually create a confirmed booking",
2494+
"parameters": [
2495+
{
2496+
"type": "integer",
2497+
"description": "Venue ID",
2498+
"name": "venueID",
2499+
"in": "path",
2500+
"required": true
2501+
},
2502+
{
2503+
"description": "Manual booking payload",
2504+
"name": "payload",
2505+
"in": "body",
2506+
"required": true,
2507+
"schema": {
2508+
"$ref": "#/definitions/main.ManualBookingPayload"
2509+
}
2510+
}
2511+
],
2512+
"responses": {
2513+
"201": {
2514+
"description": "Booking created successfully",
2515+
"schema": {
2516+
"$ref": "#/definitions/store.Booking"
2517+
}
2518+
},
2519+
"400": {
2520+
"description": "Bad Request: Invalid input or validation failed",
2521+
"schema": {}
2522+
},
2523+
"401": {
2524+
"description": "Unauthorized: Missing or invalid credentials",
2525+
"schema": {}
2526+
},
2527+
"409": {
2528+
"description": "Conflict: Time slot is already booked",
2529+
"schema": {}
2530+
},
2531+
"500": {
2532+
"description": "Internal Server Error: Could not create booking",
2533+
"schema": {}
2534+
}
2535+
}
2536+
}
2537+
},
24762538
"/venues/{venueID}/favorite": {
24772539
"post": {
24782540
"security": [
@@ -3540,6 +3602,39 @@ const docTemplate = `{
35403602
}
35413603
}
35423604
},
3605+
"main.ManualBookingPayload": {
3606+
"type": "object",
3607+
"required": [
3608+
"end_time",
3609+
"price",
3610+
"start_time"
3611+
],
3612+
"properties": {
3613+
"customer_email": {
3614+
"type": "string"
3615+
},
3616+
"customer_name": {
3617+
"type": "string",
3618+
"maxLength": 100
3619+
},
3620+
"customer_number": {
3621+
"type": "string"
3622+
},
3623+
"end_time": {
3624+
"type": "string"
3625+
},
3626+
"note": {
3627+
"type": "string",
3628+
"maxLength": 255
3629+
},
3630+
"price": {
3631+
"type": "integer"
3632+
},
3633+
"start_time": {
3634+
"type": "string"
3635+
}
3636+
}
3637+
},
35433638
"main.PlayerResponse": {
35443639
"type": "object",
35453640
"properties": {
@@ -3872,12 +3967,24 @@ const docTemplate = `{
38723967
"created_at": {
38733968
"type": "string"
38743969
},
3970+
"customer_name": {
3971+
"description": "optional",
3972+
"type": "string"
3973+
},
3974+
"customer_phone": {
3975+
"description": "optional",
3976+
"type": "string"
3977+
},
38753978
"end_time": {
38763979
"type": "string"
38773980
},
38783981
"id": {
38793982
"type": "integer"
38803983
},
3984+
"note": {
3985+
"description": "optional",
3986+
"type": "string"
3987+
},
38813988
"start_time": {
38823989
"type": "string"
38833990
},
@@ -4331,9 +4438,21 @@ const docTemplate = `{
43314438
"booking_id": {
43324439
"type": "integer"
43334440
},
4441+
"customer_name": {
4442+
"description": "optional",
4443+
"type": "string"
4444+
},
4445+
"customer_phone": {
4446+
"description": "optional",
4447+
"type": "string"
4448+
},
43344449
"end_time": {
43354450
"type": "string"
43364451
},
4452+
"note": {
4453+
"description": "optional",
4454+
"type": "string"
4455+
},
43374456
"price": {
43384457
"type": "integer"
43394458
},

0 commit comments

Comments
 (0)