Skip to content

Commit e2c276d

Browse files
committed
Part 1 of zoho integration for tenant management
1 parent 8037fa9 commit e2c276d

File tree

18 files changed

+2183
-4
lines changed

18 files changed

+2183
-4
lines changed

app/controllers/pwb/signup_controller.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,17 @@ def start
4242
return render :new
4343
end
4444

45-
result = ProvisioningService.new.start_signup(email: email)
45+
# Capture request info for Zoho CRM lead tracking
46+
zoho_request_info = {
47+
ip: request.remote_ip,
48+
utm_source: params[:utm_source],
49+
utm_medium: params[:utm_medium],
50+
utm_campaign: params[:utm_campaign]
51+
}
52+
53+
result = Pwb::User.with_zoho_request_info(zoho_request_info) do
54+
ProvisioningService.new.start_signup(email: email)
55+
end
4656

4757
if result[:success]
4858
signup_session.store_start_result(result)

app/jobs/pwb/zoho/base_job.rb

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# frozen_string_literal: true
2+
3+
module Pwb
4+
module Zoho
5+
# Base class for all Zoho sync jobs with common error handling
6+
#
7+
class BaseJob < ApplicationJob
8+
queue_as :zoho_sync
9+
10+
# Retry rate limit errors with exponential backoff
11+
retry_on Zoho::RateLimitError, wait: ->(executions) { (executions**2) * 30.seconds }, attempts: 5
12+
13+
# Retry connection errors with shorter intervals
14+
retry_on Zoho::ConnectionError, wait: 10.seconds, attempts: 3
15+
retry_on Zoho::TimeoutError, wait: 10.seconds, attempts: 3
16+
17+
# Retry API errors (might be temporary)
18+
retry_on Zoho::ApiError, wait: 30.seconds, attempts: 2
19+
20+
# Don't retry auth errors - needs manual intervention
21+
discard_on Zoho::AuthenticationError do |job, error|
22+
Rails.logger.error "[Zoho] Authentication error in #{job.class.name}: #{error.message}"
23+
Rails.logger.error "[Zoho] Check Zoho credentials and refresh token"
24+
# TODO: Send alert to admin
25+
end
26+
27+
# Don't retry config errors
28+
discard_on Zoho::ConfigurationError do |job, error|
29+
Rails.logger.warn "[Zoho] Not configured, skipping #{job.class.name}: #{error.message}"
30+
end
31+
32+
# Don't retry validation errors (bad data)
33+
discard_on Zoho::ValidationError do |job, error|
34+
Rails.logger.error "[Zoho] Validation error in #{job.class.name}: #{error.message}"
35+
end
36+
37+
private
38+
39+
def zoho_enabled?
40+
Zoho::Client.instance.configured?
41+
end
42+
43+
def lead_sync_service
44+
@lead_sync_service ||= Zoho::LeadSyncService.new
45+
end
46+
end
47+
end
48+
end
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# frozen_string_literal: true
2+
3+
module Pwb
4+
module Zoho
5+
# Sync user activity to Zoho CRM as notes + engagement score
6+
#
7+
# Tracks important user actions that indicate engagement:
8+
# - Adding properties
9+
# - Customizing website (logo, theme, pages)
10+
# - Receiving inquiries
11+
# - Adding team members
12+
#
13+
# Usage:
14+
# Pwb::Zoho::SyncActivityJob.perform_later(user.id, 'property_added', { title: 'Beach House', reference: 'BH-001' })
15+
#
16+
class SyncActivityJob < BaseJob
17+
def perform(user_id, activity_type, details = {})
18+
return unless zoho_enabled?
19+
20+
user = ::Pwb::User.find_by(id: user_id)
21+
unless user
22+
Rails.logger.warn "[Zoho] User #{user_id} not found for activity sync"
23+
return
24+
end
25+
26+
# Skip if user hasn't been synced to Zoho yet
27+
unless user.metadata&.dig('zoho_lead_id').present?
28+
Rails.logger.debug "[Zoho] User #{user_id} has no Zoho lead, skipping activity"
29+
return
30+
end
31+
32+
lead_sync_service.log_activity(user, activity_type, details.symbolize_keys)
33+
end
34+
end
35+
end
36+
end
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# frozen_string_literal: true
2+
3+
module Pwb
4+
module Zoho
5+
# Sync a new user signup to Zoho CRM as a Lead
6+
#
7+
# Triggered when a user completes signup step 1 (email submission)
8+
#
9+
# Usage:
10+
# Pwb::Zoho::SyncNewSignupJob.perform_later(user.id, { ip: '1.2.3.4', utm_source: 'google' })
11+
#
12+
class SyncNewSignupJob < BaseJob
13+
def perform(user_id, request_info = {})
14+
return unless zoho_enabled?
15+
16+
user = ::Pwb::User.find_by(id: user_id)
17+
unless user
18+
Rails.logger.warn "[Zoho] User #{user_id} not found, skipping signup sync"
19+
return
20+
end
21+
22+
# Skip if already synced
23+
if user.metadata&.dig('zoho_lead_id').present?
24+
Rails.logger.info "[Zoho] User #{user_id} already has lead, skipping"
25+
return
26+
end
27+
28+
lead_sync_service.create_lead_from_signup(user, request_info: request_info.symbolize_keys)
29+
end
30+
end
31+
end
32+
end
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# frozen_string_literal: true
2+
3+
module Pwb
4+
module Zoho
5+
# Sync subscription changes to Zoho CRM
6+
#
7+
# Handles:
8+
# - Plan selection/changes (update Lead)
9+
# - Subscription activation (convert Lead to Customer)
10+
# - Subscription cancellation (mark Lead as Lost)
11+
#
12+
# Usage:
13+
# Pwb::Zoho::SyncSubscriptionJob.perform_later(subscription.id, 'activated')
14+
#
15+
class SyncSubscriptionJob < BaseJob
16+
VALID_EVENTS = %w[created plan_changed activated canceled expired].freeze
17+
18+
def perform(subscription_id, event)
19+
return unless zoho_enabled?
20+
return unless VALID_EVENTS.include?(event.to_s)
21+
22+
subscription = ::Pwb::Subscription.find_by(id: subscription_id)
23+
unless subscription
24+
Rails.logger.warn "[Zoho] Subscription #{subscription_id} not found"
25+
return
26+
end
27+
28+
website = subscription.website
29+
user = website&.owner
30+
31+
unless user
32+
Rails.logger.warn "[Zoho] No owner found for subscription #{subscription_id}"
33+
return
34+
end
35+
36+
case event.to_s
37+
when 'created', 'plan_changed'
38+
lead_sync_service.update_lead_plan_selected(user, subscription)
39+
when 'activated'
40+
lead_sync_service.convert_lead_to_customer(user, subscription)
41+
when 'canceled'
42+
lead_sync_service.mark_lead_lost(user, 'User Canceled')
43+
when 'expired'
44+
lead_sync_service.mark_lead_lost(user, 'Trial Expired')
45+
end
46+
end
47+
end
48+
end
49+
end
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# frozen_string_literal: true
2+
3+
module Pwb
4+
module Zoho
5+
# Sync website creation to Zoho CRM (update existing Lead)
6+
#
7+
# Triggered when a user completes signup step 2 (website configuration)
8+
#
9+
# Usage:
10+
# Pwb::Zoho::SyncWebsiteCreatedJob.perform_later(user.id, website.id)
11+
#
12+
class SyncWebsiteCreatedJob < BaseJob
13+
def perform(user_id, website_id)
14+
return unless zoho_enabled?
15+
16+
user = ::Pwb::User.find_by(id: user_id)
17+
website = ::Pwb::Website.find_by(id: website_id)
18+
19+
unless user && website
20+
Rails.logger.warn "[Zoho] User #{user_id} or website #{website_id} not found"
21+
return
22+
end
23+
24+
# Get the plan from subscription if available
25+
plan = website.subscription&.plan
26+
27+
lead_sync_service.update_lead_website_created(user, website, plan)
28+
end
29+
end
30+
end
31+
end
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# frozen_string_literal: true
2+
3+
module Pwb
4+
module Zoho
5+
# Sync website going live to Zoho CRM (update Lead status)
6+
#
7+
# Triggered when website provisioning completes and email is verified
8+
#
9+
# Usage:
10+
# Pwb::Zoho::SyncWebsiteLiveJob.perform_later(user.id, website.id)
11+
#
12+
class SyncWebsiteLiveJob < BaseJob
13+
def perform(user_id, website_id)
14+
return unless zoho_enabled?
15+
16+
user = ::Pwb::User.find_by(id: user_id)
17+
website = ::Pwb::Website.find_by(id: website_id)
18+
19+
unless user && website
20+
Rails.logger.warn "[Zoho] User #{user_id} or website #{website_id} not found"
21+
return
22+
end
23+
24+
lead_sync_service.update_lead_website_live(user, website)
25+
end
26+
end
27+
end
28+
end
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# frozen_string_literal: true
2+
3+
module Pwb
4+
module Zoho
5+
# Update Zoho leads when their trial is ending soon
6+
#
7+
# This is a scheduled job that should run daily to update leads
8+
# whose trial ends in 3, 2, or 1 days.
9+
#
10+
# Usage (in config/schedule.rb or Solid Queue recurring):
11+
# Pwb::Zoho::TrialReminderJob.perform_later
12+
#
13+
class TrialReminderJob < BaseJob
14+
REMINDER_DAYS = [3, 2, 1, 0].freeze
15+
16+
def perform
17+
return unless zoho_enabled?
18+
19+
REMINDER_DAYS.each do |days|
20+
process_trials_ending_in(days)
21+
end
22+
end
23+
24+
private
25+
26+
def process_trials_ending_in(days)
27+
# Find subscriptions with trials ending in exactly `days` days
28+
target_date = Date.current + days.days
29+
30+
subscriptions = ::Pwb::Subscription
31+
.trialing
32+
.where(trial_ends_at: target_date.beginning_of_day..target_date.end_of_day)
33+
.includes(website: :owner)
34+
35+
Rails.logger.info "[Zoho] Found #{subscriptions.count} trials ending in #{days} days"
36+
37+
subscriptions.find_each do |subscription|
38+
user = subscription.website&.owner
39+
next unless user
40+
41+
lead_sync_service.update_trial_ending(user, days)
42+
rescue Zoho::Error => e
43+
Rails.logger.error "[Zoho] Failed to update trial reminder for subscription #{subscription.id}: #{e.message}"
44+
end
45+
end
46+
end
47+
end
48+
end

app/models/concerns/pwb/website_provisionable.rb

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ module WebsiteProvisionable
4141
after do
4242
update!(provisioning_started_at: Time.current) if provisioning_started_at.blank?
4343
log_provisioning_step('owner_assigned')
44+
sync_website_created_to_zoho
4445
end
4546
end
4647

@@ -105,14 +106,20 @@ module WebsiteProvisionable
105106
# Step 9: Complete registration (user created Firebase account)
106107
event :complete_owner_registration do
107108
transitions from: :locked_pending_registration, to: :live
108-
after { log_provisioning_step('live') }
109+
after do
110+
log_provisioning_step('live')
111+
sync_website_live_to_zoho
112+
end
109113
end
110114

111115
# Direct go_live (for admin use or special cases)
112116
event :go_live do
113117
transitions from: [:ready, :locked_pending_email_verification, :locked_pending_registration],
114118
to: :live, guard: :can_go_live?
115-
after { log_provisioning_step('live') }
119+
after do
120+
log_provisioning_step('live')
121+
sync_website_live_to_zoho
122+
end
116123
end
117124

118125
# Failure handling
@@ -305,5 +312,25 @@ def provisioning_failed_step_progress
305312
return 15 if has_owner?
306313
0
307314
end
315+
316+
# ===================
317+
# Zoho CRM Sync
318+
# ===================
319+
320+
def sync_website_created_to_zoho
321+
return unless owner
322+
323+
Pwb::Zoho::SyncWebsiteCreatedJob.perform_later(owner.id, id)
324+
rescue StandardError => e
325+
Rails.logger.error("[Zoho] Failed to queue website created sync: #{e.message}")
326+
end
327+
328+
def sync_website_live_to_zoho
329+
return unless owner
330+
331+
Pwb::Zoho::SyncWebsiteLiveJob.perform_later(owner.id, id)
332+
rescue StandardError => e
333+
Rails.logger.error("[Zoho] Failed to queue website live sync: #{e.message}")
334+
end
308335
end
309336
end

0 commit comments

Comments
 (0)