Skip to content

Commit

Permalink
handle case when a new booking triggers a conflict with another exist…
Browse files Browse the repository at this point in the history
…ing booking
  • Loading branch information
raphoester committed Aug 23, 2024
1 parent a16d799 commit d5d814e
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 8 deletions.
27 changes: 21 additions & 6 deletions internal/domain/commands/book_ticket/book_ticket.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package book_ticket

import (
"context"
"errors"
"fmt"

"github.com/raphoester/space-trouble-api/internal/domain/model/bookings"
Expand All @@ -10,11 +11,14 @@ import (
"github.com/raphoester/space-trouble-api/internal/pkg/id"
)

func NewBookTicket() *BookTicket {
return &BookTicket{}
func NewTicketBooker(bookingsRepository BookingsRepository) *TicketBooker {
return &TicketBooker{
bookingsRepository: bookingsRepository,
}
}

type BookTicket struct {
type TicketBooker struct {
bookingsRepository BookingsRepository
}

type BookTicketParams struct {
Expand All @@ -30,12 +34,14 @@ type BookTicketParams struct {

type BookingsRepository interface {
SaveBooking(ctx context.Context, flight *bookings.Booking) error
ListConflictingFlightBookings(ctx context.Context, booking *bookings.Booking) ([]bookings.Booking, error)
}

func (b *BookTicket) Execute(
var ErrLaunchpadUnavailable = errors.New("launchpad is already used for another destination on that day")

func (b *TicketBooker) Execute(
ctx context.Context,
params BookTicketParams,
bookingsRepository BookingsRepository,
) error {
bd, err := birthday.Parse(params.Birthday)
if err != nil {
Expand All @@ -54,7 +60,16 @@ func (b *BookTicket) Execute(
Birthday: bd,
}, params.DestinationID, params.LaunchpadID, launchDate)

if err := bookingsRepository.SaveBooking(ctx, booking); err != nil {
conflicts, err := b.bookingsRepository.ListConflictingFlightBookings(ctx, booking)
if err != nil {
return fmt.Errorf("could not check launchpad availability: %w", err)
}

if len(conflicts) != 0 {
return ErrLaunchpadUnavailable
}

if err := b.bookingsRepository.SaveBooking(ctx, booking); err != nil {
return fmt.Errorf("could not save booking: %w", err)
}

Expand Down
50 changes: 48 additions & 2 deletions internal/domain/commands/book_ticket/book_ticket_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/raphoester/space-trouble-api/internal/domain/commands/book_ticket"
"github.com/raphoester/space-trouble-api/internal/domain/model/bookings"
"github.com/raphoester/space-trouble-api/internal/infrastructure/secondary/inmemory_bookings_storage"
"github.com/raphoester/space-trouble-api/internal/pkg/date"
"github.com/raphoester/space-trouble-api/internal/pkg/id"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand All @@ -24,7 +25,7 @@ type testSuite struct {
func (s *testSuite) TestNominalCase() {
s.T().Run("Should be able to book a ticket when no conflict is detected", func(t *testing.T) {
storage := inmemory_bookings_storage.New()
bookTicket := book_ticket.NewBookTicket()
bookTicket := book_ticket.NewTicketBooker(storage)

bookingID := (&id.FixedIDGenerator{}).Generate().String()

Expand All @@ -39,7 +40,7 @@ func (s *testSuite) TestNominalCase() {
LaunchDate: "13/10/2024",
}

err := bookTicket.Execute(context.Background(), params, storage)
err := bookTicket.Execute(context.Background(), params)

assert.NoError(t, err)
savedBookings, err := storage.ListBookings(context.Background())
Expand All @@ -60,3 +61,48 @@ func (s *testSuite) TestNominalCase() {
assert.EqualValues(t, expectedSnapshot, savedBookings[0].ToSnapshot())
})
}

func (s *testSuite) TestConflict() {
s.T().Run("Should not be able to book a ticket when the booking triggers a conflict", func(t *testing.T) {
storage := inmemory_bookings_storage.New()
bookTicket := book_ticket.NewTicketBooker(storage)

idFactory := id.NewChaoticFactory(t.Name())
launchpadID := idFactory.Generate()
destinationID := idFactory.Generate()
launchDate := date.MustParse("25/11/2024")

conflictingBooking := bookings.New((&id.FixedIDGenerator{}).Generate(),
bookings.ClientData{
FirstName: "John",
LastName: "Doe",
Gender: "Male",
Birthday: "07/03",
}, destinationID.String(), launchpadID.String(), launchDate)
err := storage.SaveBooking(context.Background(), conflictingBooking)
require.NoError(t, err)

err = bookTicket.Execute(context.Background(),
book_ticket.BookTicketParams{
ID: idFactory.Generate().String(),
FirstName: "Jane",
LastName: "Smith",
Gender: "Female",
Birthday: "29/09",
LaunchpadID: launchpadID.String(),
DestinationID: idFactory.Generate().String(), // other destination
LaunchDate: launchDate.String(),
})

assert.Error(t, err)
assert.ErrorIs(t, err, book_ticket.ErrLaunchpadUnavailable)

storedBookings, err := storage.ListBookings(context.Background())
require.NoError(t, err)
require.GreaterOrEqual(t, len(storedBookings), 1)
assert.Len(t, storedBookings, 1)

firstBooking := storedBookings[0]
assert.EqualValues(t, conflictingBooking.ToSnapshot(), firstBooking.ToSnapshot())
})
}
12 changes: 12 additions & 0 deletions internal/domain/model/bookings/booking.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ func (b Booking) ID() id.ID {
return b.id
}

func (b Booking) ConflictsWith(with Booking) bool {
if b.launchpadID != with.launchpadID || b.launchDate != with.launchDate {
return false
}

if b.destinationID != with.destinationID {
return true
}

return false
}

func (b Booking) ToSnapshot() BookingSnapshot {
return BookingSnapshot{
ID: b.id.String(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,16 @@ func (s *Storage) SaveBooking(_ context.Context, booking *bookings.Booking) erro
return nil
}

func (s *Storage) ListConflictingFlightBookings(ctx context.Context, booking *bookings.Booking) ([]bookings.Booking, error) {
ret := make([]bookings.Booking, 0)
for _, v := range s.bookings {
if booking.ConflictsWith(v) {
ret = append(ret, v)
}
}
return ret, nil
}

func (s *Storage) ListBookings(_ context.Context) ([]bookings.Booking, error) {
b := make([]bookings.Booking, 0, len(s.bookings))
for _, booking := range s.bookings {
Expand Down
21 changes: 21 additions & 0 deletions internal/pkg/chaos/chaos.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package chaos

import (
"crypto/sha256"
"encoding/binary"
"math/rand"
)

func stringToSeed(s string) uint64 {
hash := sha256.Sum256([]byte(s))
return binary.BigEndian.Uint64(hash[:8])
}

func Int(seed string, cap int) int {
if cap == 0 {
return 0
}
u := stringToSeed(seed)
r := rand.New(rand.NewSource(int64(u)))
return r.Int() % cap
}
9 changes: 9 additions & 0 deletions internal/pkg/date/date.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,12 @@ func Parse(date string) (Date, error) {
}
return Date(t), nil
}

func MustParse(date string) Date {
parsed, err := Parse(date)
if err != nil {
panic(err)
}

return parsed
}
25 changes: 25 additions & 0 deletions internal/pkg/id/chaotic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package id

import (
"math/rand"

"github.com/google/uuid"
"github.com/raphoester/space-trouble-api/internal/pkg/chaos"
)

type ChaoticGenerator struct {
rnd *rand.Rand
}

func NewChaoticFactory(seed string) *ChaoticGenerator {
rnd := rand.New(rand.NewSource(int64(chaos.Int(seed, 100))))
return &ChaoticGenerator{rnd: rnd}
}

func (g *ChaoticGenerator) Generate() ID {
id, err := uuid.NewRandomFromReader(g.rnd)
if err != nil {
panic(err)
}
return ID(id.String())
}

0 comments on commit d5d814e

Please sign in to comment.