The Concept
This app is the intellectual property of Send It and its creators: Sean Fitzgerald, Christopher Cosby, Francesco Salerno and Daniel Roeser.
A friend of mine was toying with the idea of making a real application where a user can send and receive gift cards, so I thought I would try to make a real prototype using my newfound Rails prowess. Here is a video walkthrough of its basic functionality.
Setting up a Schema
One of the most fundamental requirements of this project for Flatiron was that our application have a two-way has_many_through: relationship. In database speak, this means that we should have three database tables — two tables that have a relationship to each other through a join table (the relationship being made available by the join table containing the foreign keys of the other two tables).
From one point of view, my Gifty app satisfies the has_many_through: relationship. There are three tables: Users, Gift Cards and Stores. A user has many stores through gift cards, and a store has many users through gift cards. However, the gift card model is not so straightforward.
A standard has_many_through: relationship is a one-to-many relationship from both sides. A gift card, however, in a real-world application is a little different, because a gift card belongs to two users: a sender and a recipient.
After a little research it seems this kind of relationship is very common in messaging scenarios, where a message has both a sender and a recipient. So, I modeled my gift card relationship on a similar basis.
class User < ApplicationRecord has_many :sent_gift_cards, class_name: "GiftCard", foreign_key: "sender_id" has_many :received_gift_cards, class_name: "GiftCard", foreign_key: "recipient_id" ...
The above code is how I establish my sent and received gift cards. The way this translates to ActiveRecord and to my model is that the methods user.sent-gift_cards and user.received_gift_cards become available to manage those data.
But what about the Store to Gift Card relationship? If I try the standard has_many :users, through: :gift_cards relationship, the method store.users doesn’t work, because the model GiftCard does not have a “user” column. You get this error:
ActiveRecord::HasManyThroughSourceAssociationNotFoundError (Could not find the source association(s) "user" or :users in model GiftCard. Try 'has_many :users, :through => :gift_cards, :source => <name>'. Is it one of sender, recipient, or store?)
I believe there is a way to make this relationship work through ActiveRecord macros, but in the end I found it easier to just write custom readers for Store < Users.
class Store < ApplicationRecord has_many :gift_cards def users senders = self.gift_cards.pluck(:sender_id) recipients = self.gift_cards.pluck(:recipient_id) User.find((recipients+senders).uniq) end def senders User.find(self.gift_cards.pluck(:sender_id)) end def recipients User.find(self.gift_cards.pluck(:recipient_id)) end end
Here I’ve created custom readers for all users, for senders, and for recipients. This way I’ve satisfied my store has_many :users, through: :gift_cards relationship. Now for the other side (User has_many :stores, through: :gift_cards).
class User < ApplicationRecord ... def stores giftcards = (self.sent_gift_cards + self.received_gift_cards).uniq stores = giftcards.collect {|s| s.store_id}.compact.uniq Store.find(stores) end def gift_cards GiftCard.where('sender_id = ? or recipient_id = ?', self.id, self.id) end
With the above code, I’ve added custom readers for both stores and for gift_cards. So now you can see all the stores that a User has had any relationship with through the gift_cards table. I also wrote a gift_cards reader so that you can access all gift_cards a user has a relationship with instead of calling them specifically by sent_gift_cards or received_gift_cards.
This was probably the hardest part (at least intellectually) of this project. But once I set the database up correctly, everything was pretty smooth after that.
Setting up Routes
Another requirement for this project was having nested routes, which was definitely necessary for my users < gift_cards relationship. By nesting my gift_cards routes under users I was able to attach users to only their gift_cards and vise versa. This way, in order to create a new gift card, for example, it would be from “/users/1/gift_cards/new” and the new gift_card would persist as having a relationship to User #1. Of course the recipient of this gift card will also be able to access it as the recipient, but nobody else will be able to access it without correct login credentials.
Here is my routes file:
Rails.application.routes.draw do get 'users/:user_id/gift_cards/sent', to: 'gift_cards#sent', as: 'user_sent_gift_cards' get 'gift_cards/:id', to: 'gift_cards#public_show', as: 'gift_card' resources :users, only: [:new, :create, :edit, :update, :show] do resources :gift_cards, only: [:index, :new, :create, :show] end get '/auth/facebook/callback' => 'sessions#create' get '/invite', to: 'users#new_from_gift_card' patch '/invite', to: 'users#create_from_gift_card' get 'login', to: 'sessions#new' post 'login', to: 'sessions#create' get 'welcome', to: 'sessions#welcome' delete 'sessions', to: 'sessions#destroy' root to: 'sessions#welcome' end
But What if the Recipient doesn’t Exist?
The other more complicated part of this application was what to do with a gift card that was sent to a user that doesn’t exist. Here’s what I ended up doing. When you create a new gift card as a logged in user, one of the first things my create action does in my GiftCard Controller is check whether or not the recipient exists. To put it in pseudo-code: If the recipient exists in our database, create a new gift card with that user as the recipient. Otherwise, create a new user with the email address provided and create a new gift card with that new user as the recipient.
Here’s what my create action looks like in my GiftCard Controller.
def create @card = GiftCard.new(gift_card_params) @card.assign_attributes(sender: current_user, code: generate_code) if @card.recipient = User.find_by_email(params[:gift_card][:recipient]) if @card.save flash[:notice] = ["Card successfully sent"] redirect_to user_gift_card_path(current_user.id, @card.id) else flash[:alert] = ["There was a problem with your request"] render 'gift_cards/new' end else @card.recipient = User.create(password: generate_code, email: params[:gift_card][:recipient]) @card.update(sender: current_user, code: generate_code) flash[:notice] = ["We couldn't find your friend in our system, so we've invited them to our platform.", "Card successfully created."] redirect_to user_gift_card_path(current_user.id, @card.id) end end
Although it’s not implemented here, the idea is that the new user would receive an email with the gift card code for the new gift card. The new user would also receive a link to ‘/invite’ — a link that is not available from the website views as it’s really only for this scenario. From there, the user can create a new account, but they must provide a correct gift card code that corresponds to the email address of that gift card’s recipient. From here the user can basically edit their user information that they didn’t know already existed. The user could also simply apply the gift card code to the corresponding retail site without ever having to interact with the Gifty app.
From the gift card sender side, this is what their experience looks like when they send a gift card to a user that doesn’t exist yet.
Putting it All Together
How to deal with sending gift cards to unknown users was the biggest hurdle. That and creating my weird, custom database model. There was so much minutia that went into this project that I could probably write a novella about it, but the most important challenges I wanted to convey were those of the unknown user and the model relationships.
We had to utilize a third-party login, which I did through Facebook using the omniauth gem (I hate Facebook, for the record). That went relatively smoothly.
I implemented a ton of validations and authentications to keep those cheeky hackers from hacking. For example, if you try to edit someone’s account that doesn’t belong to you, you get redirected with this flash message:
Basic Navigation
Initially I made some crude button-links for simple navigation like log in and log out. Later, I employed the ‘rails-layout’ gem, which was really helpful. It makes a simple CSS layout that includes a nav bar, so I put my navigation there. I highly recommend this gem just for some basic formatting and navigation.
I spent a little time experimenting with Bootstrap, but it was a little more involved than my limited time could spare for this project. I look forward to doing a deep dive into CSS, bootstrap and flex-box within the next couple months.
In the meantime, though, the ‘rails-layout’ gem was pretty wonderful.
Final Thoughts
Rails was a little unnerving to me because it can do so many things “automagically.” Relying on those kind of macros actually makes me kind of uncomfortable. That’s why it was nice writing my own readers for the has_many through relationship, because it sort of iterated that at the end of the day, all Rails does is provide you with a bunch of slick methods so you don’t have to do that kind of tedious work unless the circumstance really calls for it. I definitely walked away from this appreciating the power of Rails.
The domain I created for this project was also much more complex than my previous Covid Stories application. I’m glad I took on a bigger challenge in terms of database complexity.