Luigi Battaglioli

The Chocolate Milk Mile

Written by Luigi Battaglioli · 03/20/2020

What is The Chocolate Milk Mile?

To quote from their fancy new website (thechocolatemilkmile.org)

The Chocolate Milk Mile is an event dedicated to celebrating the life of Noah Farrelly, who loved running. All of the proceeds from this event will be donated to the Noah Farrelly Memorial Fund.

The event entails that participants drink a glass of chocolate milk upon completion of every lap of a mile. This adds up to be about a half gallon of chocolate milk.

Not only is it a great cause, but it's an awesome time for the community to get together and have some fun.

— Nick Hunzicker, The Chocolate Milk Mile

One of the event directors, Nick Hunzicker, is a good friend of mine as well and he was curious about learning how to build web apps. I couldn't say no to a chance to teach what I know, while also helping out a good cause. Name a more perfect duo, I'll wait. Just kidding, I'll move on now.

When Nick came to me, he originally wanted a landing page for the event. A place for people to go so they can find out more information about the event, what it's purpose is, how much it is, etc. Once the landing pages were built, he wanted to build a custom registration system for people to use to register for the event.. obviously.

On the backend, he wanted an event manager dashboard where he could get an overview of how many registrants he has signed up to run, how much money the event has brought in, and a place to check-in the registrants on the day of the event.

🤓

Nerd Alert!

I'm gonna nerd out for a bit and talk about how this application works. There will be a hefty ammount of code in here. If that's not your kinda thing, check out my other articles! I have plenty of non-code related things!

Inspired by Ron Swanson's Very Good Building and Development Co., we decided to build our own Very Good Registration Systems, Inc. Use it, or don't. End of tagline.

When we were building it, we wanted a really minimal design, that was easy to use, and it had to be really efficient when it came to checking people in on the day of the event. We decided to put in the effort and make use of real-time event broadcasting to make (almost) the whole system always in sync. This was a big learning opportunity for me because I hadn't used real time events in the past, so this was cool to play with.

Registration Process

After creating an account in the system, the registration process is pretty straight forward. The user simply selects the event they want to register for. It then shows them the registration form.

Pretty straightforward, right?

Pretty straightforward, right?

Now, let's take a quick peek at the controller to see what's goin' on behind the scenes when a user registers for the event.

public function post(Event $event){
    $this->validate(request(), [
        'name' => 'required|string',
        'email' => 'required|email|unique:registrations',
        'payment_token' => 'required|string',
        'shirtSize' => 'required_if:hasShirt,true',
    ]);

    $registration = $event->register([
        'name' => request('name'),
        'email' => request('email'),
        'mile_time' => request('mile_time'),
        'user_id' => Auth::user()->id,
    ]);

    if(request()->has('hasShirt') && request()->has('shirtSize')){
        $registration->orderShirt(request('shirtSize'));
    }

    try {
        $this->paymentGateway->charge($registration, request('payment_token'));
    } catch (PaymentFailedException $exception) {
        $registration->cancel();
        return back()->with('payment_error', 'Uh-oh! Your payment failed. Try again? If that fails, contact us!');
    }
    
    return redirect(route('registration.confirmation', [$event, $registration]));
    
}

Using the magic of Laravel's route-model binding, the controller accepts whatever Event that the user is registering for. We do some simple validation of the request, ensuring that they gave us a name, an email, filled in a credit card number, and that they gave us a shirt size if their order contains a t-shirt.

From there, we call the register method on the bound Event. If we take a look at that method, it takes in an array representing the registrant.

public function register($registrant){
    return $this->registrations()->create([
        'user_id' => $registrant['user_id'],
        'name' => $registrant['name'],
        'mile_time' => $registrant['mile_time'],
        'email' => $registrant['email'],
    ]);
}

This method simply creates a new Registration associated with this Event, and returns that newly created Registration.

Back in the controller, we go on to check if the request has a shirt order with it. The request will have a hasShirt key if the registrant wants a shirt with their order. If they do in fact want a shirt with their registration, we then call the orderShirt method on the Registration.

public function orderShirt($size){
    $this->update(['hasShirt' => true]);

    return $this->shirtOrder()->create([
        'size' => $size
    ]);
}

All that this method is doing is setting the hasShirt property of the the Registration to true, and creating a new ShirtOrder for this Registration. This method is also providing the new ShirtOrder with the size of the shirt that the user wants.

At this point we can use the PaymentGateway to charge the user for the registration. The charge method on the PaymentGateway accepts the Registration and a payment token from the front end (which is generated by Stripe). We can take a quick peek at that if you'd like:

class StripePaymentGateway implements PaymentGateway
{
    private $totalCharges;

    public function _construct(){
        Stripe::setApiKey(env('STRIPE_SECRET'));
        $this->totalCharges = collect();
    }

    public function charge(Registration $registration, $token){

        $this->totalCharges->add($registration->price);
        
        // I was on a time crunch here... Probably not the best approach. We love magic numbers.
        if($registration->hasShirtOrder()){
            $this->totalCharges->add(1300);
        }

        try {
            \Stripe\Charge::create([
                'amount' => $this->totalCharges->sum(),
                'currency' => 'usd',
                'description' => 'Event Registration Fee',
                'source' => $token,
            ]);
        }
        catch (CardException $exception)
        {
            $registration->cancel();
            throw new PaymentFailedException($exception);
        }
        catch (ApiErrorException $apiErrorException){
            $registration->cancel();
            throw new PaymentFailedException($apiErrorException);
        }

        return $registration->confirm();

    }
}

If the PaymentGateway throws a PaymentFailedException, we cancel the registration, and redirect back to the form letting the user know their payment method failed. If the charge goes through successfully, we call the confirm method on the Registration model.

public function confirm(){
    $this->update([
        'confirmed_at' => Carbon::now(),
        'confirmation_number' => ConfirmationIssuer::issueConfirmationNumber()
    ]);
}

All that confirm does is set the confirmed_at property to be the current date and time, and then it uses the ConfirmationIssuer to issue a new confirmation number to the Registration.

All that's left is to return a redirect to the confirmation page, passing the Registration along with it and displaying the finalized information to the registrant. It'll display their confirmation number and some other information about their registration.

This is that confirmation page I was telling ya about!

This is that confirmation page I was telling ya about!

Awesome! Now we've got a new registration in the system! If we were to click that blue button to go to our registrations, we'd see that newly created registration, along with a QR code.

Let's switch gears a bit and talk about the backend event management system.

The Check In Process

The process of registering for the event is a piece of cake. We wanted to make checking in on the day of the event a whole cake in and of itself. That's why we adopted the use of QR codes.

The QR code encodes the confirmation number associated with the registration. This allows the event managers who are checking people in, to simply scan the code to access the registrant's information within the system.

Within the manager dashboard, we have a check in page. On that page, there's a VueQrcode component from this cool library I stumbled upon on GitHub. This component detects when there's a QR code within the view of the webcam. When it detects one, it'll call the onDecode method. Let's take a peek at that real quick:

onDecode (confirmationNumber) {
    this.confirmationnumber = confirmationNumber;
    axios
        .get('/api/confirmation/' + this.confirmationnumber)
        .then(response => {
            this.event = response.data.event;
            this.registration = response.data.registration;
        })
        .catch(error => {

            alert('That is not a valid confirmation number. Please try again.');

        });
},

The parameter confirmationNumber, contains the value decoded from the QR code. We then go ahead and save that confirmation number to the Vue component.

Finally, once we have the confirmation number from the registrant's QR code, we make a post request to our backend to check-in the registrant with that confirmation number. On the server, that looks like this:

Route::post('/manager/check-in', function (Request $request){
    try {
        $registration = App\Registration::where('confirmation_number', $request->confirmation_number)->firstOrFail()->checkIn();
        $event = $registration->event;

        event(new App\Events\RegistrantCheckedIn($registration));

        return [
            'registration' => $registration,
            'event' => $event
        ];
    } catch (\App\Exceptions\AlreadyCheckedInException $exception){
        return response([
            'error' => 'You are already checked in.',
        ], 422);
    }
});

We first try to find the registration with the matching confirmation number. Because we're using the firstOrFail method, this will return a 404 if it can't find a registration. Once we do have the Registration however, we call the checkIn method on it.

public function checkIn(){
    if($this->checked_in_at == null){
        $this->update([
            'checked_in_at' => Carbon::now()
        ]);
        return $this;
    }
    throw new AlreadyCheckedInException();
}

The checkIn method simply sets the checked_in_at property of the Registration to be the current timestamp. It'll also check if the person is already checked in, in which case it will throw an AlreadyCheckedInException and will let the manager know they're already checked in.

After we check-in the the registrant, we fire off a new RegistrantCheckedIn event. This immediately broadcasts an event to Pusher, saying that the registrant was checked in. We can then pickup that broadcasted event on the front end using Laravel Echo, and we can then update the users registration to show they're checked in. On the user's end, we have a couple methods in our Vue component to do this.

methods: {
    getRegistrations() {
        axios
            .get('/user/api/registrations')
            .then(response => {
                this.registrations = response.data;
            });
    },

    listen() {
        window.Echo.channel('registrations')
            .listen('RegistrantCheckedIn', (e) => {
                this.getRegistrations();
            });
    }
},

mounted() {
    this.getRegistrations();
    this.listen();
},

When the page is mounted, we get the user's current registrations and display them. We then call the listen method which triggers Laravel Echo to listen for the RegistrantCheckedIn event within the registrations Pusher channel. When it picks up on a new registrant being checked in, it'll call getRegistrations again, and refresh our registration information.

Wow, you're still here?!

Wow. I applaud your commitment, and I appreciate you taking the time to read this. I hope you found it as interesting as I did. I had a blast building this with my good friend Nick, and if you're reading this, I hope you learned a little something too. And on the off chance you didn't learn anything, maybe this article will help to demystify the codebase for you. 😆