Clam Card

Level: Intermediate 30–60 min

Concepts: State

Solutions: C# | TypeScript | Python


Implement a system for a contactless travel card for subways.

  • The card does not need to be topped up.
  • The card charges the owner’s bank account directly when used.
  • The card is used by touching in and out at train stations.
  • The train system accepting this card has two categorical zones of stations, Zone A and Zone B.
Zone A Zone B
AsteriskBison
AmershamBugel
AldgateBalham
AngelBullhead
AnerleyBarbican
  • Traveling within zone B is more expensive than traveling in Zone A.
  • The price of zone B is inclusive of traveling within zone A.
  • If one of the stations is within Zone 2 at any point in a journey, the price for zone 2 will be charged.

The fares are as described below:

Zone Single Day Week Month
Zone A $2.50 $7.00 $40.00 $145.00
Zone B $3.00 $8.00 $47.00 $165.00
  • A Single is a journey from one station to another.
  • A Day fare includes all single journeys made within a single day.
  • A Week fare includes all single journeys made within a single week.
  • A Month fare includes all single journeys made within a single month.
  • No matter how many journeys are made within one of the time boundaries within a particular zone, the price will cap at that time period’s fare.

Scenarios

Follow these scenarios driving out our code as you go.

One-Way Zone 1 Journey Given Michael has a Clam Card And Michael travels from Asterisk to Aldgate Then Michael will be charged $2.50 for his first journey

One-Way Zone 1 to Zone 2 Journey Given Michael has a Clam Card And Michael travels from Asterisk to Barbican Then Michael will be charged $3.0 for his first journey

Multiple journeys Given Michael has a Clam Card And Michael travels from Asterisk to Aldgate And Michael travels from Asterisk to Balham Then Michael will be charged $2.50 for his first journey And a further $3.00 for his second journey. Totaling $5.50

Multiple Journeys including Zone B reaching daily cap Given Michael has a Clam Card And Michael travels from Asterisk to Barbican And Michael travels from Barbican to Balham And Michael travels from Balham to Bison And Michael travels from Bison to Asterisk Then Michael will be charged $3.00 for his first journey And a further $3.00 for his second journey And a further $2 for his third journey And a further $0.00 for his fourth journey

Multiple Journeys Zone A reaching daily cap Given Michael has a Clam Card And Michael travels from Asterisk to Aldgate And Michael travels from Aldgate to Angel And Michael travels from Angel to Antelope And Michael travels from Antelope to Asterisk Then Michael will be charged $2.50 for his first journey And a further $2.50 for his second journey And a further $2 for his third journey And a further $0.00 for his fourth journey

Bonus

Take into account for return journeys, the new fares are as described below:

Zone Single Return
Zone A $2.00
Zone B $2.50
  1. A Single is a journey from one station to another.
  2. A Return is a single journey where the next immediate journey is the inverse of the previous journey.
  3. Traveling from station A to station B, then from Station B to station A, classifies as a return.
  4. A Day fare still includes all single and return journeys made within a single day.
  5. A Week fare still includes all single and return journeys made within a single week.
  6. A month fare still includes all single and return journeys made within a single month.

Multiple Return Journeys And Michael travels from Asterisk to Barbican And Michael travels from Barbican to Asterisk And Michael travels from Asterisk to Balham And Michael travels from Balham to Asterisk Then Michael will be charged $3.00 for his first journey And a further $2.50 for his second journey And a further $1.50 for his third journey And a further $0.00 for his fourth journey

Reference Walkthrough

Reference implementations in C#, TypeScript, and Python live at tddbuddy-reference-katas/clam-card. This is an F2 (light builder) kata: one primary aggregate (Card), a value-type Ride that records (from, to, zone, fare), a two-case Zone enum, a single UnknownStationError for off-network stations, and two small test-folder builders (CardBuilder + RideBuilder) that stage the zone→station network and synthesise Ride literals for equality scenarios. The API reads as the spec’s sentence form: card.travelFrom("Asterisk").to("Aldgate").

Scope note — pure domain only, daily cap only. The reference covers single-journey fare calculation across the four zone combinations, cumulative totals, and the daily per-zone cap ($7.00 Zone A, $8.00 Zone B). The weekly and monthly caps, the return-journey discount (the kata’s Bonus section), a bank-account collaborator the card notifies per charge, a validated station catalogue, and any peak/off-peak or concession pricing are all out of scope and listed as stretch goals in the repo README. Weekly/monthly caps need a clock collaborator rich enough to reason about week/month boundaries; return discounts need the card to remember its previous journey and detect the inverse — both are F3-shaped extensions.

This kata ships in Agent Full-Bake mode at middle gear, the F2 (light builder) tier. See the repo’s Gears section for why middle gear is the deliberate choice.