Why have a separate Employment model?
For many use cases it's fine to have a single User
model which contains both login details and app domain specific fields but there are some cases where it makes sense to separate Employment
from User
:
- You want the user to be able to have multiple employments:
- users who can be employed by multiple companies at the same time who can use the same login details
- contractors who have multiple separate employments over time, each with their own specific employment details
- users who can have multiple concurrent relationships with a single company at the same time
- You want the user to own/manage their account and the business to own/manage the employment details
- You want to separate invitations from the user model so you can invite to specific employments or other resources
- You want the user account to be deleteable by the user but details of the employment retained for historical purposes
Read more about the reasons for separating models here.
What you'll build
This pattern demonstrates how to create an app with User
, Business
and Employment
models where a user can sign up, creating the first business and employment at the same time. Note these models could also be named User
, Team
and Membership
.
The signed in user can then invite other users to the business. Accepting an invitation to a business will allow you to access that business. Signed in users can switch between businesses they have access to.
Users can end an employment which will remove the user's access to the business.
Users can signup directly to a business.
For consistency, throughout the app, we will associate objects with the Employment
model rather than the User
model. The User
model will be used for logging in and for the user to maintain details about themselves that are shared across all employments.
App setup
We start with the pattern_base1 base app which can also be created with the following commands:
APP_NAME=employments
rails new $APP_NAME -d postgresql -c tailwind --skip-jbuilder --skip-test
cd $APP_NAME
git add -A && git commit -m "rails new $APP_NAME -d postgresql --skip-jbuilder --skip-test"
Then we add and configure the following gems to the Gemfile:
rails db:create
bundle add devise
bundle add letter_opener --group "development"
rails g devise:install
# config/environments/development.rb
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
config.action_mailer.delivery_method = :letter_opener
config.action_mailer.perform_deliveries = true
Domain model
Let's get our Devise User
model created and scaffold up our other models:
rails g devise user
rails g scaffold business name:string --no-helper
rails g scaffold employment user:references business:references role:string started_at:datetime ended_at:datetime --no-helper
rails g scaffold invite sender:belongs_to recipient:belongs_to invitable:references{polymorphic} email:string token:string accepted_at:datetime --no-helper
Then we'll tweak the employment migration to allow for a blank user. This is because we create an employment when we create an invite but we don't have a user at that point. We'll also add an index to prevent duplicate employments.
# db/migrate/20240101010000_create_employments.rb
class CreateEmployments < ActiveRecord::Migration[7.0]
def change
create_table :employments do |t|
t.references :user, null: true, foreign_key: true
t.references :business, null: false, foreign_key: true
t.string :role
t.datetime :started_at
t.datetime :ended_at
t.index [:user_id, :business_id], unique: true, where: "ended_at IS NULL AND user_id IS NOT NULL"
t.timestamps
end
end
end
In the invite migration we link sender and recipient to the employment table:
# db/migrate/20240101010000_create_invites.rb
class CreateInvites < ActiveRecord::Migration[7.0]
def change
create_table :invites do |t|
t.belongs_to :sender, null: false, foreign_key: { to_table: :employments }
t.belongs_to :recipient, null: false, foreign_key: { to_table: :employments }
t.references :invitable, polymorphic: true, null: false
t.string :email
t.string :token
t.datetime :accepted_at
t.timestamps
end
end
end
We could index email
with invitable
here but that would prevent us from re-inviting a user after their employment has ended.
Then we run the migrations with:
rails db:migrate
and add the following code to our new models:
# app/models/business.rb
class Business < ApplicationRecord
has_many :employments, dependent: :destroy
has_many :users, through: :employments
has_many :invites, as: :invitable
alias_attribute :to_s, :name
end
# app/models/user.rb
class User < ApplicationRecord
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable
has_many :employments, dependent: :destroy
has_many :businesses, through: :employments
accepts_nested_attributes_for :businesses
alias_attribute :to_s, :email
end
# app/models/employment.rb
class Employment < ApplicationRecord
ROLES = %w[owner employee].freeze
belongs_to :user, optional: true
belongs_to :business
has_many :sent_invites, class_name: :Invite, foreign_key: :sender_id, inverse_of: :sender, dependent: :destroy
has_many :received_invites, class_name: :Invite, foreign_key: :recipient_id, inverse_of: :recipient, dependent: :destroy
delegate :to_s, to: :user
scope :started, -> { where.not(started_at: nil) }
scope :not_ended, -> { where(ended_at: nil) }
scope :ended, -> { where.not(ended_at: nil) }
scope :active, -> { started.not_ended }
default_scope { not_ended }
def end
ActiveRecord::Base.transaction do
update!(ended_at: Time.current)
end
rescue StandardError
false
else
true
end
end
We use default scope here to hide ended employments everywhere in the app. You can use .unscoped
to access ended employments.
# app/models/invite.rb
class Invite < ActiveRecord::Base
belongs_to :sender, class_name: :Employment, inverse_of: :sent_invites
belongs_to :recipient, class_name: :Employment, inverse_of: :received_invites
accepts_nested_attributes_for :recipient
belongs_to :invitable, polymorphic: true
before_create :generate_token
after_commit do
destroy_unclaimed_employment and next if destroyed?
send_invite_email and next if send_invite_email?
end
validates :email, presence: true, format: { with: Devise.email_regexp }, uniqueness: { scope: [:invitable_id, :invitable_type], case_sensitive: false }
validates :invitable, presence: true
validates :sender, presence: true
validate :email_is_not_in_employments
attribute :resend, :boolean, default: false
alias_attribute :to_s, :email
def accept!(user)
ActiveRecord::Base.transaction do
now = Time.zone.now
update!(accepted_at: now)
recipient.update!(started_at: now, user:)
end
end
def send_invite_email
# InviteMailer.send(user? ? :existing_user : :new_user, self).deliver_later
end
def destroy_unclaimed_employment
recipient.destroy! if recipient&.user.nil?
end
def generate_token
self.token = SecureRandom.hex(16)
end
def send_invite_email?
recipient.previously_new_record? || resend?
end
def email_is_not_in_employments
errors.add(:email, "is already in use") if invitable.employments.excluding(recipient).includes(:user).where(user: { email: }).any?
end
def user_params
{ email: }
end
def user?
user.present?
end
def user
User.find_by(email:)
end
end
A few points to note on Invite
:
- The relevant user for invite is found at runtime as it's possible for it not to exist at the time of invite creation but for one to be created for another business before the invite is accepted.
- We validate the email address is not already in use on one of the employments for the business.
- Acceptance is inside a transaction as we need to update the invite and the employment at the same time.
Sign up flow
With the data model in place, let's setup the sign up flow. We'll overload the devise registration controller to allow for:
- The first signup which creates the first business and employment
- Invitation acceptance for a new user which creates the employment and links it to the user
- Invitation acceptance for an existing user which links the employment to the user
First let's customise the default devise registration form to accept business name and allow for an invite token:
mkdir -p app/views/devise/registrations && touch app/views/devise/registrations/new.html.erb
<!-- app/views/devise/registrations/new.html.erb -->
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), data: { turbo: false }, html: { class: 'form' }) do |f| %>
<%= render "devise/shared/error_messages", resource: resource %>
<% if @invite %>
<%= hidden_field_tag :invite_token, @invite.token %>
<p class="mb-2">You are being invited to join the <strong><%= @invite.invitable %></strong> team.</p>
<% else %>
<%= f.fields_for :businesses do |bf| %>
<div class="mt-2">
<%= bf.label :name, "Business name" %><br />
<%= bf.text_field :name %>
</div>
<% end %>
<% end %>
<% if @invite&.user? %>
<p class="mb-2">You are already a member of the following teams:</p>
<ul class="list-disc list-inside font-bold mb-2">
<% @invite.user.businesses.each do |business| %>
<li><%= business.name %></li>
<% end %>
</ul>
<p class="mb-2">You will still be able to access these teams after you accept this invitation.</p>
<%= f.submit 'Accept invitation', class: "cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" %>
<% else %>
<div class="mt-2">
<%= f.label :email %><br />
<%= f.email_field :email %>
</div>
<div class="mt-2">
<%= f.label :password %>
<% if @minimum_password_length %>
<em>(<%= @minimum_password_length %> characters minimum)</em>
<% end %>
<br />
<%= f.password_field :password, autocomplete: "new-password" %>
</div>
<div class="mt-2">
<%= f.label :password_confirmation %><br />
<%= f.password_field :password_confirmation, autocomplete: "new-password" %>
</div>
<div class="mt-4">
<%= f.submit "Sign up", class: "cursor-pointer rounded-md bg-indigo-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600" %>
</div>
<% end %>
<% end %>
We'll need to customise the devise controller as well. We first copy it into our app with:
rails g devise:controllers users -c registrations
Then we can customise it as follows:
# app/controllers/users/registrations_controller.rb
class Users::RegistrationsController < Devise::RegistrationsController
before_action :configure_sign_up_params, only: [:create]
before_action :set_invite, only: [:new, :create]
def new
super do |resource|
if params[:invite_token].present?
flash[:alert] = "Invite not found" and return unless @invite.present?
resource.assign_attributes(@invite.user_params)
else
resource.businesses.build
end
end
end
def create
if params[:invite_token].present?
flash[:alert] = "Invite not found" and return unless @invite.present?
if @invite.user?
self.resource = @invite.user
else
build_resource(sign_up_params.reverse_merge(@invite.user_params))
end
@invite.accept!(resource)
else
build_resource(sign_up_params)
resource.save
end
if resource.persisted?
if resource.active_for_authentication?
set_flash_message! :notice, :signed_up
sign_up(resource_name, resource)
respond_with resource, location: after_sign_up_path_for(resource)
else
set_flash_message! :notice, :"signed_up_but_#{resource.inactive_message}"
expire_data_after_sign_in!
respond_with resource, location: after_inactive_sign_up_path_for(resource)
end
else
clean_up_passwords resource
set_minimum_password_length
respond_with resource
end
end
protected
def set_invite
@invite = Invite.find_by(token: params[:invite_token])
end
def configure_sign_up_params
devise_parameter_sanitizer.permit(:sign_up, keys: [{ businesses_attributes: [:name] }])
end
end
We are doing a few things here:
- Building the
Business
object and allowing its params, allowing for the first user to signup, creating the first business and employment at the same time. - If an invite token is present we validate it and assign the user params to the resource for the new action
- In the create action we look for the invite and if it's for an existing user we assign the user to the resource, otherwise we build the resource with the invite user params
- The rest is default Devise code
So that we can test the user journey let's add a home page to the root app:
rails g controller pages home --no-helper
<!-- app/views/pages/home.html.erb -->
<div class="prose">
<h1 class="font-bold text-4xl">Home</h1>
<% if current_user %>
<p><%= link_to "Businesses", businesses_path %></p>
<p><%= link_to "Invites", invites_path %></p>
<p><%= link_to "Employments", employments_path %></p>
<p><%= link_to "Ended employments", ended_employments_path %></p>
<p><%= link_to "Sign out", destroy_user_session_path, data: { turbo_method: :delete } %></p>
<% else %>
<p><%= link_to "Sign in", new_user_session_path %></p>
<p><%= link_to "Sign up", new_user_registration_path %></p>
<% end %>
</div>
Now we'll tweak the routes to add devise and our home page. We'll also remove some of the default routes we dont need and add a route to list ended employments. We'll back this up with controllers and views in a later section:
Rails.application.routes.draw do
root to: "pages#home"
devise_for :users, controllers: { registrations: "users/registrations" }
resources :invites
resources :businesses, except: %i[new create destroy]
resources :employments, except: %i[new create] do
get :ended, on: :collection
end
end
You can now test the signup flow for a new user with a business.
Current employment
Let's now add a current employment to the user model. This will allow us to switch between businesses the user has access to.
rails g migration add_current_employment_to_users current_employment:belongs_to
And tweak it to reference the employments table and allow null (it's possible for all employments to end):
# db/migrate/20240101010000_add_current_employment_to_users.rb
class AddCurrentEmploymentToUsers < ActiveRecord::Migration[7.0]
def change
add_reference :users, :current_employment, null: true, foreign_key: { to_table: :employments }
end
end
rails db:migrate
Then we'll add the following to our models:
# app/models/user.rb
belongs_to :current_employment, class_name: :Employment, inverse_of: :current_users, optional: true
# app/models/employment.rb
concerning :CurrentEmployment do
included do
has_many :current_users, class_name: :User, foreign_key: :current_employment_id, inverse_of: :current_employment
after_create :set_as_current_employment
end
def set_as_current_employment
return if user.blank?
user.update!(current_employment: self)
update!(started_at: Time.current) if started_at.blank?
end
end
def end
ActiveRecord::Base.transaction do
update!(ended_at: Time.current)
user.update!(current_employment: user.employments.order(created_at: :desc).first)
end
rescue StandardError
false
else
true
end
Here we set the current employment on the user when the employment is created and we set the current employment on the user to the most recently created employment when an employment ends.
# app/models/invite.rb
def accept!(user)
ActiveRecord::Base.transaction do
now = Time.zone.now
update!(accepted_at: now)
recipient.update!(started_at: now, user:)
user.update!(current_employment: recipient)
end
end
Now let's create the current employment switcher controller:
touch app/controllers/current_user_controller.rb
# config/routes.rb
get :switch_employment, to: 'current_user#switch_employment'
# app/controllers/current_user_controller.rb
class CurrentUserController < ApplicationController
before_action :authenticate_user!
def switch_employment
current_user.update_columns(current_employment_id: params[:employment_id]) if current_user.employment_ids.include?(params[:employment_id].to_i)
redirect_to params[:redirect_to].presence || root_url
end
end
We check that the user has the employment they are trying to switch to and we also add a redirect param so we can send them back to where they came from.
Controllers and views
Let's tidy up our controllers and views to make use of the current employment and business.
First make the current employment and business available in all controllers and views:
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
helper_method :current_business, :current_employment
private
def current_employment
current_user.current_employment
end
def current_business
current_employment.business
end
end
Then tighten up the main controllers to use the current business:
# app/controllers/businesses_controller.rb
class BusinessesController < ApplicationController
before_action :set_business, only: %i[ show edit update destroy ]
def index
@businesses = base_relation
end
def show; end
def edit; end
def update
if @business.update(business_params)
redirect_to @business, notice: "Business was successfully updated.", status: :see_other
else
render :edit, status: :unprocessable_entity
end
end
private
def base_relation
current_user.businesses
end
def set_business
@business = base_relation.find(params[:id])
end
def business_params
params.require(:business).permit(:name)
end
end
# app/controllers/employments_controller.rb
class EmploymentsController < ApplicationController
before_action :set_employment, only: %i[ show edit update destroy ]
def index
@employments = base_relation
end
def ended
@employments = current_business.employments.unscope(:where).ended
end
def show; end
def edit; end
def update
if @employment.update(employment_params)
redirect_to @employment, notice: "Employment was successfully updated.", status: :see_other
else
render :edit, status: :unprocessable_entity
end
end
def destroy
if @employment.end
redirect_to ended_employments_url, notice: "Employment was successfully ended."
else
@employment.errors.add(:base, "Employment could not be ended.")
render :edit, status: :unprocessable_entity
end
end
private
def base_relation
current_business.employments.active
end
def set_employment
@employment = base_relation.find(params[:id])
end
def employment_params
params.require(:employment).permit(:user_id, :business_id, :started_at, :ended_at)
end
end
# app/controllers/invites_controller.rb
class InvitesController < ApplicationController
before_action :set_invite, only: %i[ show edit update destroy ]
def index
@invites = base_relation
end
def show; end
def new
@invite = base_relation.new(sender: current_employment)
@invite.recipient = current_business.employments.build
end
def edit; end
def create
@invite = base_relation.new({ sender: current_employment }.merge(invite_params))
@invite.recipient.business = current_business
if @invite.save
redirect_to @invite, notice: "Invite was successfully created."
else
render :new, status: :unprocessable_entity
end
end
def update
if @invite.update(invite_params)
redirect_to @invite, notice: "Invite was successfully updated.", status: :see_other
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@invite.destroy
redirect_to invites_url, notice: "Invite was successfully destroyed.", status: :see_other
end
private
def set_invite
@invite = base_relation.find(params[:id])
end
def base_relation
current_business.invites
end
def invite_params
params.require(:invite).permit(:email, :resend, recipient_attributes: [:id, :role])
end
end
Views
Let's first remove the views we don't need:
rm app/views/businesses/new.html.erb app/views/employments/new.html.erb
Now for the views let's add the recipient object fields to the invite form:
<!-- app/views/invites/_form.html.erb -->
<%= form_with(model: invite, class: "contents") do |form| %>
<% if invite.errors.any? %>
<div id="error_explanation" class="bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3">
<h2><%= pluralize(invite.errors.count, "error") %> prohibited this invite from being saved:</h2>
<ul>
<% invite.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="my-5">
<%= form.label :email %>
<%= form.email_field :email, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
</div>
<%= form.fields_for :recipient do |rf| %>
<div class="my-5">
<%= rf.label :role %>
<%= rf.select :role, Employment::ROLES, { include_blank: true }, class: "block shadow rounded-md border border-gray-200 outline-none px-3 py-2 mt-2 w-full" %>
</div>
<% end %>
<% if form.object.persisted? %>
<div class="my-5">
<%= form.check_box :resend, class: "inline-block rounded-md border border-gray-200 outline-none mr-2" %>
<%= form.label :resend %>
</div>
<% end %>
<div class="inline">
<%= form.submit class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
</div>
<% end %>
You can copy the rest of the views out of the full diff below as there are a lot of repetitive changes.
Mailer
Now let's setup the mailer and remove the text views for brevity:
rails g mailer Invite new_user existing_user
rm app/views/invite_mailer/new_user.text.erb app/views/invite_mailer/existing_user.text.erb
# app/mailers/invite_mailer.rb
class InviteMailer < ApplicationMailer
def existing_user(invite)
@invite = invite
@user_registration_url = new_user_registration_url(invite_token: @invite.token)
mail to: @invite.email
end
def new_user(invite)
@invite = invite
@user_registration_url = new_user_registration_url(invite_token: @invite.token)
mail to: @invite.email
end
end
en:
invite_mailer:
new_user:
subject: You have been invited to join us new
existing_user:
subject: You have been invited to join us existing
<!-- app/views/invite_mailer/new_user.html.erb -->
<p>Hi <%= @invite %>,</p>
<p><%= @invite.sender %> with <%= @invite.invitable %> has invited you to collaborate with them. Use the link below to set up your account and get started:</p>
<p><%= link_to "Set up account", new_user_registration_url(invite_token: @invite.token) %></p>
We use the same view for existing_user.html.erb
We can now uncomment the mailer code in the invite model:
# app/models/invite.rb
def send_invite_email
InviteMailer.send(user? ? :existing_user : :new_user, self).deliver_later
end
You can now give the invite process a test - you should receive an email with a link to sign up. Follow that in an incognito window and you will be added to the business.
Try creating another business in another browser and inviting the same email to the other business. The invitee should be able to accept then switch between businesses.
Testing
Test framework setup
We use our standard test framework setup layer which includes RSpec, FactoryBot, Cuprite and Capybara, based on the Evil Martians system of a test.
Factories
Let's add some factories:
touch {spec/factories/invites.rb,spec/factories/users.rb,spec/factories/businesses.rb,spec/factories/employments.rb}
Integration tests
touch spec/system/end_employment_spec.rb
touch spec/system/invite_existing_user_spec.rb
touch spec/system/invite_new_user_spec.rb
touch spec/system/resend_invite_spec.rb
touch spec/system/sign_up_spec.rb
touch spec/system/switch_between_employments_spec.rb
# spec/system/end_employment_spec.rb
require "system_helper"
RSpec.describe "End an employment" do
let(:business) { create(:business, :with_user) }
let(:user) { business.users.first }
let!(:other_employment) { create(:employment, business: business) }
let(:other_user) { other_employment.user }
it "allows a user to end an employment" do
using_session "User" do
sign_in user
visit employments_path
within "#employment_#{other_employment.id}" do
accept_confirm do
click_on "End this employment"
end
end
expect(page).to have_content("Employment was successfully ended.")
expect(page).to have_content("Ended employments")
expect(page).to have_content(other_user.email)
visit employments_path
expect(page).not_to have_content(other_user.email)
end
using_session "Other user" do
sign_in other_user
visit businesses_path
expect(page).not_to have_content(business.name)
end
end
end
# spec/system/invite_existing_user_spec.rb
require "system_helper"
RSpec.describe "Invite an existing user" do
let(:business) { create(:business, :with_user) }
let(:other_business) { create(:business, :with_user) }
let(:inviter) { business.users.first }
let(:invitee) { other_business.users.first }
it "allows us to invite an existing user to the system" do
sign_in inviter
visit new_invite_path
fill_in "Email", with: invitee.email
select 'employee', from: 'Role'
click_button "Create Invite"
expect(page).to have_content("Invite was successfully created.")
using_session "Other user" do
open_email(invitee.email)
expect(current_email.subject).to have_content "You have been invited to join us existing"
expect(current_email).to have_content invitee.email
expect(current_email).to have_content inviter.email
expect(current_email).to have_content business.name
current_email.click_link "Set up account"
expect(page).to have_content("You are being invited to join the #{business.name} team.")
expect(page).to have_content("You are already a member of the following teams:")
expect(page).to have_content(other_business.name)
click_on "Accept invitation"
expect(page).to have_content("Welcome! You have signed up successfully.")
click_on "Businesses"
expect(page).to have_content(business.name)
end
end
end
# spec/system/invite_new_user_spec.rb
require "system_helper"
RSpec.describe "Invite a new user to a business" do
let(:business) { create(:business, :with_user) }
let(:inviter) { business.users.first }
let(:invitee) { build(:user) }
it "allows a user to sign up as a business" do
sign_in inviter
visit new_invite_path
fill_in "Email", with: invitee.email
select 'employee', from: 'Role'
click_button "Create Invite"
expect(page).to have_content("Invite was successfully created.")
using_session "Other user" do
open_email(invitee.email)
expect(current_email.subject).to have_content "You have been invited to join us new"
expect(current_email).to have_content invitee.email
expect(current_email).to have_content inviter.email
expect(current_email).to have_content business.name
current_email.click_on "Set up account"
expect(page).to have_content("You are being invited to join the #{business.name} team.")
expect(page).to have_field("Email", with: invitee.email)
fill_in "Password", with: invitee.password
fill_in "Password confirmation", with: invitee.password
click_on "Sign up"
expect(page).to have_content("Welcome! You have signed up successfully.")
click_on "Businesses"
expect(page).to have_content(business.name)
click_on "Home"
click_on "Employments"
expect(page).to have_content(inviter.email)
expect(page).to have_content(invitee.email)
end
end
end
# spec/system/resend_invite_spec.rb
require "system_helper"
RSpec.describe "Resend invite" do
let(:business) { create(:business, :with_user) }
let(:user) { business.users.first }
let!(:invite) { create(:invite, invitable: business, sender: business.employments.first) }
it "allows a business to resend an invite unedited" do
clear_emails
sign_in user
visit invites_path
click_on "Edit this invite"
check "Resend"
click_on "Update Invite"
expect(page).to have_content("Invite was successfully updated")
open_email(invite.email)
expect(current_email.subject).to have_content "You have been invited to join us new"
end
it "allows a business to resend an invite and edit it" do
new_email = "new@email.com"
clear_emails
sign_in user
visit invites_path
click_on "Edit this invite"
fill_in "Email", with: new_email
check "Resend"
click_on "Update Invite"
expect(page).to have_content("Invite was successfully updated")
expect(page).to have_content(new_email)
open_email(new_email)
expect(current_email.subject).to have_content "You have been invited to join us new"
end
end
# spec/system/sign_up_spec.rb
require "system_helper"
RSpec.describe "Signup with Business" do
let(:business) { build(:business) }
let(:user) { build(:user) }
it "allows a user to sign up as a business" do
visit new_user_registration_path
fill_in "Business name", with: business.name
fill_in "Email", with: user.email
fill_in "Password", with: user.password
fill_in "Password confirmation", with: user.password
click_button "Sign up"
expect(page).to have_content("Welcome! You have signed up successfully.")
click_on "Businesses"
expect(page).to have_content(business.name)
click_on "Home"
click_on "Employments"
expect(page).to have_content(user.email)
click_on "Home"
click_on "Sign out"
expect(page).to have_content("Signed out successfully.")
end
end
# spec/system/switch_between_employments_spec.rb
require "system_helper"
RSpec.describe "Switch between employments" do
let(:business) { create(:business, :with_user) }
let(:other_business) { create(:business, :with_user) }
let(:user) { business.users.first }
let(:other_user) { other_business.users.first }
let!(:employment) { create(:employment, :started, user: other_user, business:) }
it "allows a user to switch between multiple employments" do
sign_in other_user
visit root_path
expect(page).to have_content("Current business: #{business.name}")
click_on "Switch to: #{other_business.name}"
expect(page).to have_content("Current business: #{other_business.name}")
click_on "Switch to: #{business.name}"
expect(page).to have_content("Current business: #{business.name}")
end
end
Todo
- Users can signup directly to a business.
- Domain model graphic
- add fname and lname
- also need to handle signing up directly
- Separate current_employment