Skip to content

Commit 852a632

Browse files
authored
Add checklist functionality for company onboarding (#409)
1 parent bf0bfa5 commit 852a632

File tree

19 files changed

+710
-14
lines changed

19 files changed

+710
-14
lines changed

backend/app/controllers/internal/companies/workers_controller.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ def create
1717
).perform
1818

1919
if result[:success]
20-
render json: { success: true, new_user_id: result[:company_worker].user_id, document_id: result[:document].id }, status: :ok
20+
render json: { success: true, new_user_id: result[:company_worker].user_id, document_id: result[:document]&.id }, status: :ok
2121
else
2222
render json: result, status: :unprocessable_entity
2323
end
2424
end
2525

2626
private
2727
def worker_params
28-
params.require(:contractor).permit(:email, :started_at, :pay_rate_in_subunits, :role, :pay_rate_type)
28+
params.require(:contractor).permit(:email, :started_at, :pay_rate_in_subunits, :role, :pay_rate_type, :contract_signed_elsewhere)
2929
end
3030
end

backend/app/models/company.rb

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,19 @@ class Company < ApplicationRecord
2626
self.const_set("ACCESS_ROLE_#{access_role.upcase}", access_role)
2727
end
2828

29+
ADMIN_CHECKLIST_ITEMS = [
30+
{ key: "add_company_details", title: "Add company details", description: "Add your company name and basic information" },
31+
{ key: "add_bank_account", title: "Add bank account", description: "Connect your bank account to enable payments" },
32+
{ key: "invite_contractor", title: "Invite a contractor", description: "Add your first team member" },
33+
{ key: "send_first_payment", title: "Send your first payment", description: "Process your first contractor payment" }
34+
].freeze
35+
36+
WORKER_CHECKLIST_ITEMS = [
37+
{ key: "fill_tax_information", title: "Fill tax information", description: "Complete your tax details" },
38+
{ key: "add_payout_information", title: "Add payout information", description: "Set up your payment method" },
39+
{ key: "sign_contract", title: "Sign contract", description: "Review and sign your contractor agreement" }
40+
].freeze
41+
2942
has_many :company_administrators
3043
has_many :administrators, through: :company_administrators, source: :user
3144
has_many :company_lawyers
@@ -98,7 +111,6 @@ def active
98111
after_create_commit :create_balance!
99112
after_update_commit :update_convertible_implied_shares, if: :saved_change_to_fully_diluted_shares?
100113

101-
102114
accepts_nested_attributes_for :expense_categories
103115

104116
delegate :stripe_setup_intent, :bank_account_last_four, :microdeposit_verification_required?,
@@ -190,6 +202,28 @@ def json_flag?(flag)
190202
json_data&.dig("flags")&.include?(flag)
191203
end
192204

205+
def checklist_items(user)
206+
case user
207+
when CompanyAdministrator
208+
ADMIN_CHECKLIST_ITEMS.map do |item|
209+
item.merge(completed: checklist_item_completed?(item[:key], user))
210+
end
211+
when CompanyWorker
212+
WORKER_CHECKLIST_ITEMS.map do |item|
213+
item.merge(completed: checklist_item_completed?(item[:key], user))
214+
end
215+
else
216+
[]
217+
end
218+
end
219+
220+
def checklist_completion_percentage(user)
221+
completed_count = checklist_items(user).count { |item| item[:completed] }
222+
return 0 if checklist_items(user).empty?
223+
224+
(completed_count.to_f / checklist_items(user).size * 100).round
225+
end
226+
193227
private
194228
def update_convertible_implied_shares
195229
convertible_investments.each do |investment|
@@ -211,4 +245,25 @@ def fetch_or_create_stripe_customer_id!
211245
update!(stripe_customer_id: stripe_customer.id)
212246
stripe_customer_id
213247
end
248+
249+
def checklist_item_completed?(key, user)
250+
case key
251+
when "add_company_details"
252+
name.present?
253+
when "add_bank_account"
254+
bank_account_ready?
255+
when "invite_contractor"
256+
company_workers.active.exists?
257+
when "send_first_payment"
258+
invoices.where(status: Invoice::PAID_OR_PAYING_STATES).exists?
259+
when "fill_tax_information"
260+
user.user.compliance_info&.tax_information_confirmed_at.present?
261+
when "add_payout_information"
262+
user.user.bank_account.present?
263+
when "sign_contract"
264+
user.contract_signed?
265+
else
266+
false
267+
end
268+
end
214269
end

backend/app/models/company_worker.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ def end_contract!
9898
update!(ended_at: Time.current)
9999
end
100100

101+
def contract_signed?
102+
contract_signed_elsewhere ||
103+
user.documents.joins(:signatures)
104+
.where(documents: { document_type: Document.document_types[:consulting_contract], deleted_at: nil, company: company })
105+
.where.not(document_signatures: { signed_at: nil })
106+
.exists?
107+
end
108+
101109
def quickbooks_entity
102110
"Vendor"
103111
end

backend/app/presenters/user_presenter.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ def logged_in_user
125125
primaryAdminName: company.primary_admin.user.name,
126126
completedPaymentMethodSetup: company.bank_account_ready?,
127127
isTrusted: company.is_trusted,
128+
checklistItems: company.checklist_items(user.company_administrator_for(company) || user.company_worker_for(company)),
129+
checklistCompletionPercentage: company.checklist_completion_percentage(user.company_administrator_for(company) || user.company_worker_for(company)),
128130
}
129131
end,
130132
id: user.external_id,

backend/spec/models/company_spec.rb

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,213 @@
5858
end
5959
end
6060

61+
describe "checklist functionality" do
62+
let(:company) { create(:company, :without_bank_account) }
63+
64+
describe "#checklist_items" do
65+
context "for company administrators" do
66+
let(:company) { create(:company, :pre_onboarding) }
67+
let(:admin) { create(:company_administrator, company: company) }
68+
69+
it "returns admin checklist items with completion status" do
70+
items = company.checklist_items(admin)
71+
72+
expect(items).to have_attributes(size: 4)
73+
expect(items.map { |item| item[:key] }).to contain_exactly(
74+
"add_company_details", "add_bank_account", "invite_contractor", "send_first_payment"
75+
)
76+
expect(items.all? { |item| item[:completed] == false }).to be true
77+
end
78+
79+
it "completes company details item when company name is present" do
80+
items = company.checklist_items(admin)
81+
company_details_item = items.find { |item| item[:key] == "add_company_details" }
82+
expect(company_details_item[:completed]).to be false
83+
84+
company.update!(name: "Test Company")
85+
items = company.reload.checklist_items(admin)
86+
company_details_item = items.find { |item| item[:key] == "add_company_details" }
87+
expect(company_details_item[:completed]).to be true
88+
end
89+
90+
it "completes bank account item when stripe account becomes ready" do
91+
items = company.checklist_items(admin)
92+
bank_account_item = items.find { |item| item[:key] == "add_bank_account" }
93+
expect(bank_account_item[:completed]).to be false
94+
95+
stripe_account = create(:company_stripe_account, company: company, status: "processing")
96+
items = company.reload.checklist_items(admin)
97+
bank_account_item = items.find { |item| item[:key] == "add_bank_account" }
98+
expect(bank_account_item[:completed]).to be false
99+
100+
stripe_account.update!(status: "ready")
101+
items = company.reload.checklist_items(admin)
102+
bank_account_item = items.find { |item| item[:key] == "add_bank_account" }
103+
expect(bank_account_item[:completed]).to be true
104+
end
105+
106+
it "completes contractor item when worker is created" do
107+
items = company.checklist_items(admin)
108+
contractor_item = items.find { |item| item[:key] == "invite_contractor" }
109+
expect(contractor_item[:completed]).to be false
110+
111+
create(:company_worker, company: company, user: create(:user))
112+
items = company.reload.checklist_items(admin)
113+
contractor_item = items.find { |item| item[:key] == "invite_contractor" }
114+
expect(contractor_item[:completed]).to be true
115+
end
116+
117+
it "completes payment item when payment succeeds" do
118+
items = company.checklist_items(admin)
119+
payment_item = items.find { |item| item[:key] == "send_first_payment" }
120+
expect(payment_item[:completed]).to be false
121+
122+
contractor = create(:company_worker, company: company)
123+
company.update!(name: "Test Company")
124+
invoice = create(:invoice, company: company, company_worker: contractor, user: contractor.user)
125+
items = company.reload.checklist_items(admin)
126+
payment_item = items.find { |item| item[:key] == "send_first_payment" }
127+
expect(payment_item[:completed]).to be false
128+
129+
invoice.update!(status: Invoice::PAID)
130+
items = company.reload.checklist_items(admin)
131+
payment_item = items.find { |item| item[:key] == "send_first_payment" }
132+
expect(payment_item[:completed]).to be true
133+
end
134+
135+
it "marks all admin items as completed when conditions are met" do
136+
company.update!(name: "Test Company")
137+
create(:company_stripe_account, company: company, status: "ready")
138+
contractor = create(:company_worker, company: company)
139+
invoice = create(:invoice, company: company, company_worker: contractor, user: contractor.user)
140+
invoice.update!(status: Invoice::PAID)
141+
company.reload
142+
143+
items = company.checklist_items(admin)
144+
expect(items.all? { |item| item[:completed] }).to be true
145+
end
146+
end
147+
148+
context "for company workers" do
149+
let(:worker) { create(:company_worker, without_contract: true, company: company, user: create(:user, without_bank_account: true)) }
150+
151+
it "returns worker checklist items with completion status" do
152+
items = company.checklist_items(worker)
153+
154+
expect(items).to have_attributes(size: 3)
155+
expect(items.map { |item| item[:key] }).to contain_exactly(
156+
"fill_tax_information", "add_payout_information", "sign_contract"
157+
)
158+
expect(items.all? { |item| item[:completed] == false }).to be true
159+
end
160+
161+
it "completes tax information item when worker confirms tax details" do
162+
items = company.checklist_items(worker)
163+
tax_item = items.find { |item| item[:key] == "fill_tax_information" }
164+
expect(tax_item[:completed]).to be false
165+
166+
worker.user.compliance_info.update!(tax_information_confirmed_at: Time.current)
167+
168+
items = company.checklist_items(worker)
169+
tax_item = items.find { |item| item[:key] == "fill_tax_information" }
170+
expect(tax_item[:completed]).to be true
171+
end
172+
173+
it "completes payout information item when worker adds bank account" do
174+
items = company.checklist_items(worker)
175+
payout_item = items.find { |item| item[:key] == "add_payout_information" }
176+
expect(payout_item[:completed]).to be false
177+
178+
create(:wise_recipient, user: worker.user, used_for_invoices: true)
179+
worker.user.reload
180+
181+
items = company.checklist_items(worker)
182+
payout_item = items.find { |item| item[:key] == "add_payout_information" }
183+
expect(payout_item[:completed]).to be true
184+
end
185+
186+
it "completes contract item when worker signs contract" do
187+
items = company.checklist_items(worker)
188+
contract_item = items.find { |item| item[:key] == "sign_contract" }
189+
expect(contract_item[:completed]).to be false
190+
191+
create(:document, company: company, document_type: :consulting_contract, signatories: [worker.user])
192+
193+
items = company.checklist_items(worker)
194+
contract_item = items.find { |item| item[:key] == "sign_contract" }
195+
expect(contract_item[:completed]).to be true
196+
end
197+
end
198+
199+
context "for other user types" do
200+
let(:other_user) { create(:user) }
201+
202+
it "returns empty array for non-company users" do
203+
items = company.checklist_items(other_user)
204+
expect(items).to be_empty
205+
end
206+
end
207+
end
208+
209+
describe "#checklist_completion_percentage" do
210+
context "for company administrators" do
211+
let(:company) { create(:company, :pre_onboarding) }
212+
let(:admin) { create(:company_administrator, company: company) }
213+
214+
it "returns 0 when no items are completed" do
215+
expect(company.checklist_completion_percentage(admin)).to eq(0)
216+
end
217+
218+
it "returns correct percentage when some items are completed" do
219+
create(:company_stripe_account, company: company, status: "ready")
220+
company.reload
221+
expect(company.checklist_completion_percentage(admin)).to eq(25)
222+
end
223+
224+
it "returns 100 when all items are completed" do
225+
company.update!(name: "Test Company")
226+
create(:company_stripe_account, company: company, status: "ready")
227+
contractor = create(:company_worker, company: company)
228+
invoice = create(:invoice, company: company, user: contractor.user)
229+
invoice.update!(status: Invoice::PAID)
230+
company.reload
231+
232+
expect(company.checklist_completion_percentage(admin)).to eq(100)
233+
end
234+
end
235+
236+
context "for company workers" do
237+
let(:worker) { create(:company_worker, without_contract: true, company: company, user: create(:user, without_bank_account: true)) }
238+
239+
it "returns 0 when no items are completed" do
240+
expect(company.checklist_completion_percentage(worker)).to eq(0)
241+
end
242+
243+
it "returns correct percentage when some items are completed" do
244+
worker.user.compliance_info.update!(tax_information_confirmed_at: Time.current)
245+
246+
expect(company.checklist_completion_percentage(worker)).to eq(33)
247+
end
248+
249+
it "returns 100 when all worker items are completed" do
250+
worker.user.compliance_info.update!(tax_information_confirmed_at: Time.current)
251+
create(:wise_recipient, user: worker.user, used_for_invoices: true)
252+
create(:document, company: company, document_type: :consulting_contract, signatories: [worker.user])
253+
254+
expect(company.checklist_completion_percentage(worker)).to eq(100)
255+
end
256+
end
257+
258+
context "for other user types" do
259+
let(:other_user) { create(:user) }
260+
261+
it "returns 0 for non-company users" do
262+
expect(company.checklist_completion_percentage(other_user)).to eq(0)
263+
end
264+
end
265+
end
266+
end
267+
61268
describe "validations" do
62269
it { is_expected.to validate_presence_of(:email) }
63270
it { is_expected.to validate_presence_of(:country_code) }

0 commit comments

Comments
 (0)