diff --git a/internal/hbooking/handlers/create_booking.go b/internal/hbooking/handlers/create_booking.go index 4b13977..773975b 100644 --- a/internal/hbooking/handlers/create_booking.go +++ b/internal/hbooking/handlers/create_booking.go @@ -1,8 +1,9 @@ package handlers import ( + "context" "errors" - "net/http" + "fmt" "strconv" "time" @@ -11,7 +12,7 @@ import ( "github.com/spatecon/hbooking/internal/hbooking/domain" ) -type Booking struct { +type CreateBookingResponse struct { ID string `json:"id,omitempty"` WorkshopID int64 `json:"workshop_id"` ClientID string `json:"client_id"` @@ -21,95 +22,120 @@ type Booking struct { } type CreateBookingRequest struct { - Booking + WorkshopID int64 `json:"workshop_id"` + ClientID string `json:"client_id"` + BeginAt string `json:"begin_at"` + EndAt string `json:"end_at"` + ClientTimezone string `json:"client_timezone"` } func (h *Handlers) CreateBooking(c *gin.Context) { var req CreateBookingRequest - err := c.ShouldBindJSON(&req) - if BadRequest(c, err, "failed to bind request") { + if BadRequest(c, makeCreateBookingRequest(c, &req)) { return } - workshopID, err := strconv.ParseInt(c.Param("workshop_id"), 10, 64) - if BadRequest(c, err, "failed to parse workshop_id") { + var booking domain.Booking + if BadRequest(c, requestToDomainBooking(&req, &booking)) { return } - booking := &domain.Booking{ - WorkshopID: workshopID, - ClientID: req.ClientID, - } + b, err := createBookingService(c.Request.Context(), h.repo, &booking) + if err != nil { + if ValidationFailed(c, err) { + return + } - booking.ClientTimezone, err = time.LoadLocation(req.ClientTimezone) - if BadRequest(c, err, "failed to load client timezone") { + InternalServerError(c, err) return } + c.JSON(200, makeCreateBookingResponse(b)) +} + +func makeCreateBookingRequest(c *gin.Context, r *CreateBookingRequest) error { + var err error + if err = c.ShouldBindJSON(r); err != nil { + return fmt.Errorf("failed to bind request: %w", err) + } + + if r.WorkshopID, err = strconv.ParseInt(c.Param("workshop_id"), 10, 64); err != nil { + return fmt.Errorf("failed to parse workshop_id") + } + + return nil +} + +func requestToDomainBooking(req *CreateBookingRequest, booking *domain.Booking) error { + clientTimezone, err := time.LoadLocation(req.ClientTimezone) + if err != nil { + return fmt.Errorf("failed to load client timezone") + } + beginAt, err := time.Parse("02-01-2006 15:04", req.BeginAt) - if BadRequest(c, err, "failed to parse begin_at") { - return + if err != nil { + return fmt.Errorf("failed to parse begin_at") } endAt, err := time.Parse("02-01-2006 15:04", req.EndAt) - if BadRequest(c, err, "failed to parse end_at") { - return + if err != nil { + return fmt.Errorf("failed to parse end_at") } + booking.WorkshopID = req.WorkshopID + booking.ClientID = req.ClientID booking.BeginAt = time.Date( beginAt.Year(), beginAt.Month(), beginAt.Day(), beginAt.Hour(), beginAt.Minute(), 0, 0, - booking.ClientTimezone, - ).In(booking.ClientTimezone) - + clientTimezone, + ).In(clientTimezone) booking.EndAt = time.Date( endAt.Year(), endAt.Month(), endAt.Day(), endAt.Hour(), endAt.Minute(), 0, 0, - booking.ClientTimezone, - ).In(booking.ClientTimezone) + clientTimezone, + ).In(clientTimezone) + booking.ClientTimezone = clientTimezone + + return nil +} +func createBookingService(ctx context.Context, repo Repository, booking *domain.Booking) (*domain.Booking, error) { now := time.Now().In(booking.ClientTimezone) - if ValidationFailed(c, booking.BeginAt.Before(now), "begin_at is in the past") { - return + if booking.BeginAt.Before(now) { + return nil, ValidationErrorStr("begin_at is in the past") } - if ValidationFailed(c, booking.EndAt.Before(booking.BeginAt), "end_at is before begin_at") { - return + if booking.EndAt.Before(booking.BeginAt) { + return nil, ValidationErrorStr("end_at is before begin_at") } duration := booking.EndAt.Sub(booking.BeginAt) - if ValidationFailed(c, - duration < minBookingDuration || duration > maxBookingDuration, - "invalid booking duration: must be in range [30m, 4h]", - ) { - return + if duration < minBookingDuration || duration > maxBookingDuration { + return nil, ValidationErrorStr("invalid booking duration: must be in range [30m, 4h]") } - booking, err = h.repo.CreateBooking(c.Request.Context(), booking) + b, err := repo.CreateBooking(ctx, booking) if err != nil { - if errors.Is(err, domain.ErrBookingOverlap) { - Error(c, err, http.StatusBadRequest) - return - } - if errors.Is(err, domain.ErrBookingOutOfWorkshopSchedule) { - Error(c, err, http.StatusBadRequest) - return + if errors.Is(err, domain.ErrBookingOverlap) || errors.Is(err, domain.ErrBookingOutOfWorkshopSchedule) { + return nil, ValidationError(err) } - Error(c, err, http.StatusInternalServerError) - return + return nil, err } + return b, nil +} - c.JSON(200, Booking{ - ID: booking.ID.String(), - WorkshopID: booking.WorkshopID, - ClientID: booking.ClientID, - BeginAt: booking.BeginAt.Format("02-01-2006 15:04"), - EndAt: booking.EndAt.Format("02-01-2006 15:04"), - ClientTimezone: booking.ClientTimezone.String(), - }) +func makeCreateBookingResponse(b *domain.Booking) CreateBookingResponse { + return CreateBookingResponse{ + ID: b.ID.String(), + WorkshopID: b.WorkshopID, + ClientID: b.ClientID, + BeginAt: b.BeginAt.Format("02-01-2006 15:04"), + EndAt: b.EndAt.Format("02-01-2006 15:04"), + ClientTimezone: b.ClientTimezone.String(), + } } diff --git a/internal/hbooking/handlers/error.go b/internal/hbooking/handlers/error.go index 2c50874..a0caf67 100644 --- a/internal/hbooking/handlers/error.go +++ b/internal/hbooking/handlers/error.go @@ -11,22 +11,51 @@ type ErrorResponse struct { Error string `json:"error"` } -func ValidationFailed(c *gin.Context, cond bool, description string) bool { - if !cond { +type serviceError struct { + err error + msg string +} + +func (e serviceError) Error() string { + return e.msg +} + +func (e serviceError) Unwrap() error { + return e.err +} + +func ValidationError(err error) serviceError { + return serviceError{msg: "validation failed: " + err.Error(), err: err} +} + +func ValidationErrorStr(err string) serviceError { + return ValidationError(fmt.Errorf(err)) +} + +func ValidationFailed(c *gin.Context, err error) bool { + if err == nil { + return false + } + + if _, ok := err.(serviceError); !ok { return false } - err := fmt.Errorf("validation failed: %s", description) return Error(c, err, http.StatusUnprocessableEntity) } -func BadRequest(c *gin.Context, err error, description string) bool { +func BadRequest(c *gin.Context, err error) bool { + return Error(c, err, http.StatusBadRequest) +} + +func InternalServerError(c *gin.Context, err error) bool { if err == nil { return false } - err = fmt.Errorf("%s: %w", description, err) - return Error(c, err, http.StatusBadRequest) + _ = c.Error(err) + c.Status(http.StatusInternalServerError) + return true } func Error(c *gin.Context, err error, code int) bool { diff --git a/internal/hbooking/handlers/list.go b/internal/hbooking/handlers/list.go deleted file mode 100644 index 7daa161..0000000 --- a/internal/hbooking/handlers/list.go +++ /dev/null @@ -1,42 +0,0 @@ -package handlers - -import ( - "net/http" - "strconv" - - "github.com/gin-gonic/gin" -) - -type ListBookingsRequest struct { - WorkshopID int `json:"workshop_id"` -} - -type ListBookingsResponse struct { - Bookings []*Booking `json:"bookings"` -} - -func (h *Handlers) ListBookings(c *gin.Context) { - workshopID, err := strconv.ParseInt(c.Param("workshop_id"), 10, 64) - if BadRequest(c, err, "failed to parse workshop_id") { - return - } - - bookings, err := h.repo.ListBookings(c.Request.Context(), workshopID) - if Error(c, err, http.StatusInternalServerError) { - return - } - - respBookings := make([]*Booking, 0, len(bookings)) - for _, b := range bookings { - respBookings = append(respBookings, &Booking{ - WorkshopID: b.WorkshopID, - ClientID: b.ClientID, - BeginAt: b.BeginAt.Format("02-01-2006 15:04"), - EndAt: b.EndAt.Format("02-01-2006 15:04"), - ClientTimezone: b.ClientTimezone.String(), - }) - } - - c.JSON(200, ListBookingsResponse{Bookings: respBookings}) - -} diff --git a/internal/hbooking/handlers/list_booking.go b/internal/hbooking/handlers/list_booking.go new file mode 100644 index 0000000..c41fa23 --- /dev/null +++ b/internal/hbooking/handlers/list_booking.go @@ -0,0 +1,64 @@ +package handlers + +import ( + "fmt" + "strconv" + + "github.com/gin-gonic/gin" + + "github.com/spatecon/hbooking/internal/hbooking/domain" +) + +type ListBookingsRequest struct { + WorkshopID int64 `json:"workshop_id"` +} + +type ListBookingsResponse struct { + Bookings []ListBookingResponse `json:"bookings"` +} + +type ListBookingResponse struct { + WorkshopID int64 `json:"workshop_id"` + ClientID string `json:"client_id"` + BeginAt string `json:"begin_at"` + EndAt string `json:"end_at"` + ClientTimezone string `json:"client_timezone"` +} + +func (h *Handlers) ListBookings(c *gin.Context) { + var req ListBookingsRequest + if BadRequest(c, makeListBookingsRequest(c, &req)) { + return + } + + bookings, err := h.repo.ListBookings(c.Request.Context(), req.WorkshopID) + if InternalServerError(c, err) { + return + } + + c.JSON(200, makeListBookingsResponse(bookings)) + +} + +func makeListBookingsRequest(c *gin.Context, r *ListBookingsRequest) error { + var err error + if r.WorkshopID, err = strconv.ParseInt(c.Param("workshop_id"), 10, 64); err != nil { + return fmt.Errorf("failed to parse workshop_id") + } + + return nil +} + +func makeListBookingsResponse(bookings []*domain.Booking) ListBookingsResponse { + respBookings := make([]ListBookingResponse, len(bookings)) + for i, b := range bookings { + respBookings[i] = ListBookingResponse{ + WorkshopID: b.WorkshopID, + ClientID: b.ClientID, + BeginAt: b.BeginAt.Format("02-01-2006 15:04"), + EndAt: b.EndAt.Format("02-01-2006 15:04"), + ClientTimezone: b.ClientTimezone.String(), + } + } + return ListBookingsResponse{Bookings: respBookings} +}