Skip to content

Commit 117562d

Browse files
committed
Add power user feature & ngrok auto-start
1 parent 2dff5e0 commit 117562d

File tree

16 files changed

+750
-1
lines changed

16 files changed

+750
-1
lines changed

CLAUDE.md

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
This is a Ruby on Rails 8.0 application built on the Bullet Train framework. It integrates with ClickFunnels for user management and power user tracking.
7+
8+
## Development Commands
9+
10+
### Setup
11+
```bash
12+
bin/setup # Initial setup (installs dependencies, creates database)
13+
bin/rails db:seed # Seed database with sample data
14+
```
15+
16+
### Running the Application
17+
```bash
18+
bin/dev # Start development server with Procfile.dev (includes Sidekiq, CSS/JS watching)
19+
bin/rails server # Start only Rails server
20+
```
21+
22+
### Testing
23+
```bash
24+
bin/rails test # Run all tests
25+
bin/rails test test/path/to/specific_test.rb # Run specific test file
26+
bin/rails test test/path/to/test.rb:42 # Run specific test at line 42
27+
MAGIC_TEST=1 bin/rails test:system # Record system tests with Magic Test
28+
```
29+
30+
### Code Quality
31+
```bash
32+
bundle exec standardrb # Run Ruby linter
33+
bundle exec standardrb --fix # Auto-fix Ruby linting issues
34+
```
35+
36+
### Database
37+
```bash
38+
bin/rails db:migrate # Run pending migrations
39+
bin/rails db:rollback # Rollback last migration
40+
bin/rails db:schema:dump # Update db/schema.rb
41+
```
42+
43+
### Background Jobs
44+
```bash
45+
bundle exec sidekiq # Run Sidekiq (included in bin/dev)
46+
```
47+
48+
### Deployment
49+
```bash
50+
bin/kamal deploy # Deploy to production using Kamal
51+
```
52+
53+
## Architecture Overview
54+
55+
### Bullet Train Framework
56+
Bullet Train is a comprehensive Ruby on Rails SaaS framework that provides extensive built-in functionality. This application leverages:
57+
58+
**Core Features**:
59+
- Multi-tenancy with Teams and membership-based permissions
60+
- User authentication with Devise
61+
- OAuth integration via Doorkeeper
62+
- Webhook handling (incoming and outgoing)
63+
- Admin panel via Avo
64+
- API versioning structure
65+
- Role-based access control with granular permissions
66+
- Onboarding workflows
67+
- Billing system integration (Stripe)
68+
- Zapier integration support
69+
70+
**Super Scaffolding**:
71+
Bullet Train's code generation engine that creates complete CRUD interfaces. Key commands:
72+
73+
```bash
74+
# Generate new model with CRUD interface
75+
rails generate super_scaffold ModelName Team field:field_type
76+
77+
# Add field to existing model
78+
rails generate super_scaffold:field ModelName field:field_type
79+
80+
# Available field types: text_field, trix_editor, buttons, super_select, image
81+
```
82+
83+
Example:
84+
```bash
85+
# Create a Project model under Team
86+
rails generate super_scaffold Project Team name:text_field description:trix_editor
87+
rake db:migrate
88+
89+
# Add status field with button options
90+
rails generate super_scaffold:field Project status:buttons
91+
rake db:migrate
92+
```
93+
94+
**Important Conventions**:
95+
- All models are typically scoped to Teams
96+
- Keep magic comments in generated files for future scaffolding
97+
- Use `valid_*` methods for defining select field options
98+
- Support for Action Models for complex business logic
99+
- Modular design allows overriding framework components
100+
101+
### Key Components
102+
103+
**ClickFunnels Integration** (app/lib/clickfunnels/):
104+
- Custom Faraday-based client for ClickFunnels API
105+
- Plans to migrate to official clickfunnels-ruby-sdk gem (see PLAN.md)
106+
- Webhook handling for ClickFunnels events
107+
- Background job for marking power users
108+
109+
**API Structure** (app/controllers/api/v1/):
110+
- RESTful API with versioning
111+
- OAuth authentication via Doorkeeper
112+
- Standard JSON responses
113+
114+
**Webhook System** (app/controllers/webhooks/):
115+
- Incoming webhooks from external services
116+
- Outgoing webhooks for event notifications
117+
- Signature verification for security
118+
119+
**Background Processing**:
120+
- Sidekiq for async job processing
121+
- Redis for job queue storage
122+
- MarkPowerUserJob for ClickFunnels integration
123+
124+
### Configuration
125+
126+
**Environment Variables** (config/application.yml):
127+
- BASE_URL: Application base URL
128+
- MARKETING_SITE_URL: Marketing site URL
129+
- STRIPE_SECRET_KEY: Stripe API key
130+
- CLICK_FUNNELS_API_KEY: ClickFunnels API key
131+
- CLICK_FUNNELS_WORKSPACE_SUBDOMAIN: ClickFunnels subdomain (to be renamed)
132+
- CLICK_FUNNELS_WORKSPACE_ID: Workspace ID (to be moved to config)
133+
134+
**Database**: PostgreSQL with ActiveRecord
135+
136+
**Asset Pipeline**:
137+
- ESBuild for JavaScript bundling
138+
- Tailwind CSS with PostCSS
139+
- Stimulus for JavaScript framework
140+
141+
### Testing Strategy
142+
- Minitest for unit and integration tests
143+
- Capybara for system tests
144+
- FactoryBot for test data
145+
- SimpleCov for coverage reporting
146+
- Fixtures for external API responses (test/fixtures/clickfunnels/)
147+
148+
### Current Development Focus
149+
The `mark-as-power-user-in-clickfunnels` branch implements functionality to mark users as "power users" in ClickFunnels. There's an active migration plan (PLAN.md) to move from the custom implementation to the official ClickFunnels SDK.

Gemfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,4 +270,7 @@ gem "active_hash"
270270
# You can add any Ruby gems you need below. By keeping them separate from our gems above, you'll avoid the likelihood
271271
# that you run into a merge conflict in the future.
272272

273+
# ClickFunnels SDK for interacting with the ClickFunnels API.
274+
gem "clickfunnels-ruby-sdk"
275+
273276
# 🚅 super scaffolding will insert new oauth providers above this line.

Gemfile.lock

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ GEM
223223
choice (0.2.0)
224224
chronic (0.10.2)
225225
chunky_png (1.4.0)
226+
clickfunnels-ruby-sdk (0.1.0)
227+
json (~> 2.0)
226228
cloudinary (2.3.0)
227229
faraday (>= 2.0.1, < 3.0.0)
228230
faraday-follow_redirects (~> 0.3.0)
@@ -714,6 +716,7 @@ DEPENDENCIES
714716
capybara-email
715717
capybara-lockstep
716718
chronic
719+
clickfunnels-ruby-sdk
717720
colorize
718721
cssbundling-rails
719722
debug

Procfile.dev

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ worker: bundle exec sidekiq -t 25
33
js: THEME="light" yarn build --watch
44
light-css: yarn light:build:css --watch
55
light-mailer-css: yarn light:build:mailer:css --watch
6-
# ngrok: ngrok http 3003
6+
ngrok: bin/ngrok-with-webhook

app/clients/click_funnels.rb

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
require "cf"
2+
3+
# ClickFunnels client using the official clickfunnels-ruby-sdk
4+
class ClickFunnels
5+
attr_reader :workspace_id, :subdomain, :api_key
6+
7+
def initialize(api_key: nil, workspace_id: nil, subdomain: nil)
8+
# TODO: Only API secret should be env var. The others are configs.
9+
@workspace_id = workspace_id || ENV["CLICK_FUNNELS_WORKSPACE_ID"]
10+
@api_key = api_key || ENV["CLICK_FUNNELS_API_KEY"]
11+
@subdomain = subdomain || ENV["CLICK_FUNNELS_WORKSPACE_SUBDOMAIN"]
12+
13+
raise ArgumentError, "ClickFunnels workspace_id is required" unless @workspace_id
14+
raise ArgumentError, "ClickFunnels subdomain is required" unless @subdomain
15+
raise ArgumentError, "ClickFunnels api_key is required" unless @api_key
16+
17+
# TODO: This should happen in an initializer.
18+
configure_sdk
19+
end
20+
21+
# List contacts with optional email filter
22+
# https://developers.myclickfunnels.com/reference/listcontacts
23+
#
24+
# @param email_address [String] Optional email address to filter contacts
25+
# @return [Array] Array of contact hashes from ClickFunnels API
26+
def list_contacts(email_address: nil)
27+
params = {}
28+
29+
if email_address
30+
# Try different filter format based on the SDK documentation
31+
params[:filter] = {email_address: email_address}
32+
end
33+
34+
params[:workspace_id] = workspace_id
35+
36+
response = CF::Workspaces::Contact.list(params)
37+
38+
# TODO: The SDK should return indifferent access hashes directly.
39+
Array(response).map { |item| item.to_h.with_indifferent_access }
40+
rescue => e
41+
Rails.logger.error("Error listing contacts: #{e.class} - #{e.message}") if defined?(Rails)
42+
[]
43+
end
44+
45+
# Find a contact by email address
46+
# @param email_address [String] Email address to find contact
47+
# @return [Hash, nil] Contact data or nil if not found
48+
def find_contact_by_email(email_address)
49+
contacts = list_contacts(email_address: email_address)
50+
51+
if contacts.is_a?(Array) && !contacts.empty?
52+
contacts.first.to_h.with_indifferent_access
53+
end
54+
rescue => e
55+
Rails.logger.warn("Error finding contact by email: #{e.class} - #{e.message}") if defined?(Rails)
56+
nil
57+
end
58+
59+
# Create a tag for a contact
60+
# https://developers.myclickfunnels.com/reference/createcontactsappliedtags
61+
#
62+
# @param contact_id [String] ID of the contact to tag
63+
# @param tag_id [String] ID of the tag to apply (defaults to power user tag 328935)
64+
# @return [Hash] Response from ClickFunnels API
65+
def create_contact_tag(contact_id, tag_id: "328935")
66+
response = CF::Contacts::AppliedTag.create(
67+
contact_id: contact_id,
68+
tag_id: tag_id
69+
)
70+
71+
# Convert OpenStruct response to hash with string keys for compatibility
72+
# TODO: The SDK should return indifferent access hashes directly.
73+
response.to_h.with_indifferent_access
74+
rescue => e
75+
Rails.logger.error("Error creating contact tag: #{e.class} - #{e.message}") if defined?(Rails)
76+
{"error" => e.message}
77+
end
78+
79+
# Mark a user as a power user
80+
# @param email_address [String] Email address of the user to mark as power user
81+
# @return [Boolean] True if successful, false otherwise
82+
#
83+
# FIXME: Any errors happening here and in the callstack bewow are unexpected and
84+
# should be sent to the error tracker.
85+
def mark_as_power_user(email_address)
86+
contact = find_contact_by_email(email_address)
87+
# TODO: This is an inconsistency between the BusinessOS and app, worth sending to the error tracker.
88+
unless contact
89+
Rails.logger.error("Contact not found to mark as power user for email: #{email_address}")
90+
return false
91+
end
92+
93+
contact_id = contact["id"]
94+
response = create_contact_tag(contact_id)
95+
96+
if response.is_a?(Hash) && response["error"]
97+
Rails.logger.error("Error marking user as power user: #{response["error"]}")
98+
raise "Failed to mark user as power user: #{response["error"]}"
99+
end
100+
101+
# TODO: The SDK should return indifferent access hashes directly.
102+
response.to_h.with_indifferent_access
103+
end
104+
105+
def base_url
106+
"https://#{subdomain}.myclickfunnels.com"
107+
end
108+
109+
private
110+
111+
def configure_sdk
112+
CF.configure do |config|
113+
config.subdomain = subdomain
114+
config.api_token = api_key
115+
config.workspace_id = workspace_id
116+
config.debug = Rails.env.development? if defined?(Rails)
117+
end
118+
end
119+
end

app/jobs/mark_power_user_job.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# frozen_string_literal: true
2+
3+
class MarkPowerUserJob < ApplicationJob
4+
queue_as :default
5+
6+
# Mark a user as a power user in ClickFunnels
7+
# @param email [String] Email address of the user to mark as power user
8+
def perform(email)
9+
click_funnels_client = ClickFunnels.new
10+
click_funnels_client.mark_as_power_user(email)
11+
rescue => e
12+
Rails.logger.error("Failed to mark user as power user in ClickFunnels: #{e.class} - #{e.message}")
13+
raise e
14+
end
15+
end

bin/ngrok-with-webhook

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env bash
2+
3+
# Start ngrok in background
4+
ngrok http 3003 &
5+
NGROK_PID=$!
6+
7+
# Give ngrok time to start
8+
sleep 5
9+
10+
# Update webhook URL
11+
bin/update-webhook-url
12+
13+
# Keep ngrok running
14+
wait $NGROK_PID

0 commit comments

Comments
 (0)