Kata: Conference tickets - purchases

In the previous log, we have started a tickets reservation system.

At this point, we have only supported the ticket part, ensuring there were enough tickets.

Let's handle next rules!

Rule 2: Tickets have a price

We could edit one more time our data structure, but the current one is simple enough to focus on its responsibility.

We can add an extra layer to handle extra attributes, as follows:

-- describe "Purchases" $ do
--   describe "toTickets" $ do
--     it "with Alice has one ticket" $
--       toTickets (Purchase $ Map.fromList [("Alice", Map.singleton 1 50)])
--         `shouldBe` [Attendee $ Set.singleton 1]
--     it "with Alice has one ticket and Bob booked two days" $
--       toTickets (Purchase $ Map.fromList [("Alice", Map.singleton 1 50), ("Bob", Map.fromList [(1, 50), (3, 80)])])
--         `shouldBe` [Attendee $ Set.singleton 1, Attendee $ Set.fromList [1, 3]]

newtype Purchase
  = Purchase {unPurchase :: Map.Map AttendeeName (Map.Map Int TicketPrice)}

newtype AttendeeName
  = AttendeeName Text
  deriving newtype (IsString, Eq, Ord)

newtype TicketPrice
  = TicketPrice {eurTicketPrice :: Int}
  deriving newtype (Num)

toTickets :: Purchase -> [Attendee]
toTickets = map (Attendee . Map.keysSet) . Map.elems . (.unPurchase)

Rule 3: There could be two tickets, with different prices, for the same day

This one is a bit ambiguous, it could suggest that we have a dedicated data-type for tickets, but, in the mean time, our code is handling different ticket prices, regardless the days.

Rule 4: There could be multiple days-tickets

This one is more explicit, tickets are now more complex, having a price and a set of days, we can parameterize (both parametrically in Purchase and ad-hoc in toTickets), as follows:

-- describe "Purchases" $ do
--   describe "toTickets" $ do
--     it "with Alice has one ticket" $
--       toTickets (Purchase $ Map.fromList [("Alice", Set.singleton Conference)])
--         `shouldBe` [Attendee $ Set.singleton 1]
--     it "with Alice has one ticket and Bob booked two days" $
--       toTickets (Purchase $ Map.fromList [("Alice", Set.singleton Conference), ("Bob", Set.singleton TwoDays)])
--         `shouldBe` [Attendee $ Set.singleton 1, Attendee $ Set.fromList [1, 2]]

newtype Purchase ticket
  = Purchase {unPurchase :: Map.Map AttendeeName (Set.Set ticket)}

class (Ord a) => Ticket a where
  price :: a -> TicketPrice
  ticketDays :: a -> Set.Set Int

toTickets :: (Ticket ticket) => Purchase ticket -> [Attendee]
toTickets = map (Attendee . foldMap ticketDays) . Map.elems . (.unPurchase)

data LyonCraft = Conference | Unconference | TwoDays
  deriving stock (Eq, Ord, Show)

instance Ticket LyonCraft where
  price =
    \case
      Conference -> 50
      Unconference -> 80
      TwoDays -> 120
  ticketDays =
    \case
      Conference -> Set.singleton 1
      Unconference -> Set.singleton 2
      TwoDays -> Set.fromList [1, 2]

It both represents the user-facing tickets and gives extensibility for any upcoming ticket rules.

Next time, we will have a look at how we can prevent not coherent configuration (e.g. a daily limit of 10 with a total limit of 1)