Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 75 additions & 49 deletions internal/hbooking/handlers/create_booking.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package handlers

import (
"context"
"errors"
"net/http"
"fmt"
"strconv"
"time"

Expand All @@ -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"`
Expand All @@ -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(),
}
}
41 changes: 35 additions & 6 deletions internal/hbooking/handlers/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
42 changes: 0 additions & 42 deletions internal/hbooking/handlers/list.go

This file was deleted.

64 changes: 64 additions & 0 deletions internal/hbooking/handlers/list_booking.go
Original file line number Diff line number Diff line change
@@ -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}
}