referral_box 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +27 -0
- data/LICENSE.txt +21 -0
- data/README.md +194 -0
- data/app/assets/javascripts/referral_box.js +138 -0
- data/app/controllers/referral_box/dashboard_controller.rb +91 -0
- data/app/controllers/referral_box/tracking_controller.rb +30 -0
- data/app/helpers/referral_box/application_helper.rb +56 -0
- data/app/helpers/referral_box/dashboard_helper.rb +41 -0
- data/app/views/layouts/referral_box/dashboard.html.erb +233 -0
- data/config/routes.rb +14 -0
- data/db/migrate/001_create_referral_box_transactions.rb +21 -0
- data/db/migrate/002_create_referral_box_referral_logs.rb +28 -0
- data/lib/generators/referral_box/install/install_generator.rb +45 -0
- data/lib/generators/referral_box/install/templates/initializer.rb +63 -0
- data/lib/generators/referral_box/install/templates/user_model.rb +45 -0
- data/lib/referral_box/configuration.rb +32 -0
- data/lib/referral_box/engine.rb +31 -0
- data/lib/referral_box/models/referral_log.rb +109 -0
- data/lib/referral_box/models/transaction.rb +39 -0
- data/lib/referral_box/version.rb +5 -0
- data/lib/referral_box.rb +191 -0
- metadata +233 -0
@@ -0,0 +1,233 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<title>ReferralBox Dashboard</title>
|
7
|
+
<style>
|
8
|
+
* {
|
9
|
+
margin: 0;
|
10
|
+
padding: 0;
|
11
|
+
box-sizing: border-box;
|
12
|
+
}
|
13
|
+
|
14
|
+
body {
|
15
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
16
|
+
background: #f8fafc;
|
17
|
+
color: #1e293b;
|
18
|
+
line-height: 1.6;
|
19
|
+
}
|
20
|
+
|
21
|
+
.navbar {
|
22
|
+
background: #1e293b;
|
23
|
+
color: white;
|
24
|
+
padding: 1rem 2rem;
|
25
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
26
|
+
}
|
27
|
+
|
28
|
+
.navbar h1 {
|
29
|
+
font-size: 1.5rem;
|
30
|
+
font-weight: 600;
|
31
|
+
}
|
32
|
+
|
33
|
+
.container {
|
34
|
+
max-width: 1200px;
|
35
|
+
margin: 0 auto;
|
36
|
+
padding: 2rem;
|
37
|
+
}
|
38
|
+
|
39
|
+
.sidebar {
|
40
|
+
background: white;
|
41
|
+
border-radius: 8px;
|
42
|
+
padding: 1.5rem;
|
43
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
44
|
+
margin-bottom: 2rem;
|
45
|
+
}
|
46
|
+
|
47
|
+
.nav-links {
|
48
|
+
display: flex;
|
49
|
+
gap: 1rem;
|
50
|
+
flex-wrap: wrap;
|
51
|
+
}
|
52
|
+
|
53
|
+
.nav-links a {
|
54
|
+
color: #64748b;
|
55
|
+
text-decoration: none;
|
56
|
+
padding: 0.5rem 1rem;
|
57
|
+
border-radius: 6px;
|
58
|
+
transition: all 0.2s;
|
59
|
+
}
|
60
|
+
|
61
|
+
.nav-links a:hover,
|
62
|
+
.nav-links a.active {
|
63
|
+
background: #f1f5f9;
|
64
|
+
color: #1e293b;
|
65
|
+
}
|
66
|
+
|
67
|
+
.card {
|
68
|
+
background: white;
|
69
|
+
border-radius: 8px;
|
70
|
+
padding: 1.5rem;
|
71
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
72
|
+
margin-bottom: 1.5rem;
|
73
|
+
}
|
74
|
+
|
75
|
+
.card h2 {
|
76
|
+
font-size: 1.25rem;
|
77
|
+
font-weight: 600;
|
78
|
+
margin-bottom: 1rem;
|
79
|
+
color: #1e293b;
|
80
|
+
}
|
81
|
+
|
82
|
+
.stats-grid {
|
83
|
+
display: grid;
|
84
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
85
|
+
gap: 1rem;
|
86
|
+
margin-bottom: 2rem;
|
87
|
+
}
|
88
|
+
|
89
|
+
.stat-card {
|
90
|
+
background: white;
|
91
|
+
padding: 1.5rem;
|
92
|
+
border-radius: 8px;
|
93
|
+
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
94
|
+
text-align: center;
|
95
|
+
}
|
96
|
+
|
97
|
+
.stat-number {
|
98
|
+
font-size: 2rem;
|
99
|
+
font-weight: 700;
|
100
|
+
color: #3b82f6;
|
101
|
+
margin-bottom: 0.5rem;
|
102
|
+
}
|
103
|
+
|
104
|
+
.stat-label {
|
105
|
+
color: #64748b;
|
106
|
+
font-size: 0.875rem;
|
107
|
+
text-transform: uppercase;
|
108
|
+
letter-spacing: 0.05em;
|
109
|
+
}
|
110
|
+
|
111
|
+
.table {
|
112
|
+
width: 100%;
|
113
|
+
border-collapse: collapse;
|
114
|
+
margin-top: 1rem;
|
115
|
+
}
|
116
|
+
|
117
|
+
.table th,
|
118
|
+
.table td {
|
119
|
+
padding: 0.75rem;
|
120
|
+
text-align: left;
|
121
|
+
border-bottom: 1px solid #e2e8f0;
|
122
|
+
}
|
123
|
+
|
124
|
+
.table th {
|
125
|
+
background: #f8fafc;
|
126
|
+
font-weight: 600;
|
127
|
+
color: #475569;
|
128
|
+
}
|
129
|
+
|
130
|
+
.table tr:hover {
|
131
|
+
background: #f8fafc;
|
132
|
+
}
|
133
|
+
|
134
|
+
.badge {
|
135
|
+
display: inline-block;
|
136
|
+
padding: 0.25rem 0.5rem;
|
137
|
+
border-radius: 4px;
|
138
|
+
font-size: 0.75rem;
|
139
|
+
font-weight: 500;
|
140
|
+
text-transform: uppercase;
|
141
|
+
}
|
142
|
+
|
143
|
+
.badge.earn {
|
144
|
+
background: #dcfce7;
|
145
|
+
color: #166534;
|
146
|
+
}
|
147
|
+
|
148
|
+
.badge.redeem {
|
149
|
+
background: #fef2f2;
|
150
|
+
color: #dc2626;
|
151
|
+
}
|
152
|
+
|
153
|
+
.badge.silver {
|
154
|
+
background: #f1f5f9;
|
155
|
+
color: #475569;
|
156
|
+
}
|
157
|
+
|
158
|
+
.badge.gold {
|
159
|
+
background: #fef3c7;
|
160
|
+
color: #92400e;
|
161
|
+
}
|
162
|
+
|
163
|
+
.badge.platinum {
|
164
|
+
background: #e0e7ff;
|
165
|
+
color: #3730a3;
|
166
|
+
}
|
167
|
+
|
168
|
+
.btn {
|
169
|
+
display: inline-block;
|
170
|
+
padding: 0.5rem 1rem;
|
171
|
+
border-radius: 6px;
|
172
|
+
text-decoration: none;
|
173
|
+
font-weight: 500;
|
174
|
+
transition: all 0.2s;
|
175
|
+
border: none;
|
176
|
+
cursor: pointer;
|
177
|
+
}
|
178
|
+
|
179
|
+
.btn-primary {
|
180
|
+
background: #3b82f6;
|
181
|
+
color: white;
|
182
|
+
}
|
183
|
+
|
184
|
+
.btn-primary:hover {
|
185
|
+
background: #2563eb;
|
186
|
+
}
|
187
|
+
|
188
|
+
.pagination {
|
189
|
+
display: flex;
|
190
|
+
justify-content: center;
|
191
|
+
gap: 0.5rem;
|
192
|
+
margin-top: 2rem;
|
193
|
+
}
|
194
|
+
|
195
|
+
.pagination a {
|
196
|
+
padding: 0.5rem 0.75rem;
|
197
|
+
border: 1px solid #e2e8f0;
|
198
|
+
border-radius: 4px;
|
199
|
+
text-decoration: none;
|
200
|
+
color: #64748b;
|
201
|
+
}
|
202
|
+
|
203
|
+
.pagination a:hover {
|
204
|
+
background: #f1f5f9;
|
205
|
+
}
|
206
|
+
|
207
|
+
.pagination .current {
|
208
|
+
background: #3b82f6;
|
209
|
+
color: white;
|
210
|
+
border-color: #3b82f6;
|
211
|
+
}
|
212
|
+
</style>
|
213
|
+
</head>
|
214
|
+
<body>
|
215
|
+
<nav class="navbar">
|
216
|
+
<h1>📦 ReferralBox Dashboard</h1>
|
217
|
+
</nav>
|
218
|
+
|
219
|
+
<div class="container">
|
220
|
+
<div class="sidebar">
|
221
|
+
<nav class="nav-links">
|
222
|
+
<%= link_to "Dashboard", referral_box.root_path, class: current_page?(referral_box.root_path) ? "active" : "" %>
|
223
|
+
<%= link_to "Users", referral_box.users_path, class: current_page?(referral_box.users_path) ? "active" : "" %>
|
224
|
+
<%= link_to "Transactions", referral_box.transactions_path, class: current_page?(referral_box.transactions_path) ? "active" : "" %>
|
225
|
+
<%= link_to "Referrals", referral_box.referrals_path, class: current_page?(referral_box.referrals_path) ? "active" : "" %>
|
226
|
+
<%= link_to "Analytics", referral_box.analytics_path, class: current_page?(referral_box.analytics_path) ? "active" : "" %>
|
227
|
+
</nav>
|
228
|
+
</div>
|
229
|
+
|
230
|
+
<%= yield %>
|
231
|
+
</div>
|
232
|
+
</body>
|
233
|
+
</html>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ReferralBox::Engine.routes.draw do
|
4
|
+
root to: "dashboard#index"
|
5
|
+
|
6
|
+
get "users", to: "dashboard#users"
|
7
|
+
get "users/:id", to: "dashboard#user", as: :user
|
8
|
+
get "transactions", to: "dashboard#transactions"
|
9
|
+
get "referrals", to: "dashboard#referrals"
|
10
|
+
get "analytics", to: "dashboard#analytics"
|
11
|
+
|
12
|
+
# Tracking endpoint for device data collection
|
13
|
+
post "track_referral", to: "tracking#track_referral", as: :track_referral
|
14
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateReferralBoxTransactions < ActiveRecord::Migration[7.0]
|
4
|
+
def change
|
5
|
+
create_table :referral_box_transactions do |t|
|
6
|
+
t.references :user, polymorphic: true, null: false, index: true
|
7
|
+
t.integer :points, null: false
|
8
|
+
t.string :transaction_type, null: false
|
9
|
+
t.json :event_data
|
10
|
+
t.json :offer_data
|
11
|
+
t.datetime :expires_at
|
12
|
+
t.text :description
|
13
|
+
|
14
|
+
t.timestamps
|
15
|
+
end
|
16
|
+
|
17
|
+
add_index :referral_box_transactions, :transaction_type
|
18
|
+
add_index :referral_box_transactions, :expires_at
|
19
|
+
add_index :referral_box_transactions, :created_at
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class CreateReferralBoxReferralLogs < ActiveRecord::Migration[7.0]
|
4
|
+
def change
|
5
|
+
create_table :referral_box_referral_logs do |t|
|
6
|
+
t.string :referral_code, null: false
|
7
|
+
t.references :referee, polymorphic: true, null: true
|
8
|
+
t.string :user_agent
|
9
|
+
t.string :ip_address
|
10
|
+
t.string :referrer
|
11
|
+
t.datetime :clicked_at, null: false
|
12
|
+
t.datetime :signed_up_at
|
13
|
+
t.string :device_type
|
14
|
+
t.string :browser
|
15
|
+
t.json :geo_data
|
16
|
+
t.json :device_data
|
17
|
+
|
18
|
+
t.timestamps
|
19
|
+
end
|
20
|
+
|
21
|
+
add_index :referral_box_referral_logs, :referral_code
|
22
|
+
add_index :referral_box_referral_logs, :clicked_at
|
23
|
+
add_index :referral_box_referral_logs, :signed_up_at
|
24
|
+
add_index :referral_box_referral_logs, :ip_address
|
25
|
+
add_index :referral_box_referral_logs, :device_type
|
26
|
+
add_index :referral_box_referral_logs, :browser
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rails/generators"
|
4
|
+
|
5
|
+
module ReferralBox
|
6
|
+
module Generators
|
7
|
+
class InstallGenerator < Rails::Generators::Base
|
8
|
+
source_root File.expand_path("templates", __dir__)
|
9
|
+
|
10
|
+
def create_initializer
|
11
|
+
template "initializer.rb", "config/initializers/referral_box.rb"
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_user_model_migration
|
15
|
+
generate_migration = ask("Would you like to add referral_code and tier columns to your User model? (y/n)")
|
16
|
+
|
17
|
+
if generate_migration.downcase == 'y'
|
18
|
+
generate "migration", "AddReferralBoxToUsers referral_code:string tier:string referrer:references"
|
19
|
+
puts "Migration generated! Run 'rails db:migrate' to apply it."
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_user_model_methods
|
24
|
+
user_class = ask("What is your user model class name? (default: User)")
|
25
|
+
user_class = "User" if user_class.blank?
|
26
|
+
|
27
|
+
template "user_model.rb", "app/models/#{user_class.downcase}.rb", skip: true
|
28
|
+
puts "Please add the ReferralBox methods to your #{user_class} model manually."
|
29
|
+
end
|
30
|
+
|
31
|
+
def show_instructions
|
32
|
+
puts "\n" + "="*60
|
33
|
+
puts "ReferralBox Installation Complete!"
|
34
|
+
puts "="*60
|
35
|
+
puts "\nNext steps:"
|
36
|
+
puts "1. Run 'rails db:migrate' to create the database tables"
|
37
|
+
puts "2. Add ReferralBox methods to your User model (see template)"
|
38
|
+
puts "3. Configure ReferralBox in config/initializers/referral_box.rb"
|
39
|
+
puts "4. Visit /referral_box to access the admin dashboard"
|
40
|
+
puts "\nFor more information, see the documentation!"
|
41
|
+
puts "="*60
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ReferralBox.configure do |config|
|
4
|
+
# Define which model represents your app's user/customer
|
5
|
+
config.reference_class_name = 'User' # or 'Customer', 'Account', etc.
|
6
|
+
|
7
|
+
# Custom earning rule - earn points based on events
|
8
|
+
config.earning_rule = ->(user, event) do
|
9
|
+
# Example: earn 10 points per ₹100 spent
|
10
|
+
# event.amount / 10
|
11
|
+
|
12
|
+
# Default: earn 1 point per event
|
13
|
+
1
|
14
|
+
end
|
15
|
+
|
16
|
+
# Custom redeem rule - how many points to deduct
|
17
|
+
config.redeem_rule = ->(user, offer) do
|
18
|
+
# Example: offer.cost_in_points
|
19
|
+
offer&.cost_in_points || 100
|
20
|
+
end
|
21
|
+
|
22
|
+
# Define tier thresholds
|
23
|
+
config.tier_thresholds = {
|
24
|
+
"Silver" => 500,
|
25
|
+
"Gold" => 1000,
|
26
|
+
"Platinum" => 2500
|
27
|
+
}
|
28
|
+
|
29
|
+
# Reward modifier based on tier
|
30
|
+
config.reward_modifier = ->(user) do
|
31
|
+
case user.tier
|
32
|
+
when "Silver" then 1.0
|
33
|
+
when "Gold" then 1.2
|
34
|
+
when "Platinum" then 1.5
|
35
|
+
else 1.0
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Referral reward logic
|
40
|
+
config.referral_reward = ->(referrer, referee) do
|
41
|
+
ReferralBox.earn_points(referrer, 100)
|
42
|
+
ReferralBox.earn_points(referee, 50)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Points expiration (in days)
|
46
|
+
config.points_expiry_days = 90
|
47
|
+
|
48
|
+
# Referral code length
|
49
|
+
config.referral_code_length = 8
|
50
|
+
|
51
|
+
# Admin dashboard route path
|
52
|
+
config.admin_route_path = "/referral_box"
|
53
|
+
|
54
|
+
# Device tracking and analytics
|
55
|
+
config.enable_device_tracking = true # Enable device detection
|
56
|
+
config.collect_geo_location = false # Enable geo-location (requires user consent)
|
57
|
+
|
58
|
+
# Optional: Callback when user's tier changes
|
59
|
+
config.on_tier_changed = ->(user, old_tier, new_tier) do
|
60
|
+
# Example: Send notification email
|
61
|
+
# UserMailer.tier_changed(user, old_tier, new_tier).deliver_later
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class User < ApplicationRecord
|
4
|
+
# Add these columns to your users table:
|
5
|
+
# add_column :users, :referral_code, :string
|
6
|
+
# add_column :users, :tier, :string
|
7
|
+
# add_index :users, :referral_code, unique: true
|
8
|
+
|
9
|
+
has_many :referral_box_transactions, class_name: 'ReferralBox::Transaction', as: :user
|
10
|
+
has_many :referrals, class_name: 'User', foreign_key: 'referrer_id'
|
11
|
+
belongs_to :referrer, class_name: 'User', optional: true
|
12
|
+
|
13
|
+
before_create :generate_referral_code
|
14
|
+
|
15
|
+
def points_balance
|
16
|
+
ReferralBox.balance(self)
|
17
|
+
end
|
18
|
+
|
19
|
+
def current_tier
|
20
|
+
ReferralBox.tier(self)
|
21
|
+
end
|
22
|
+
|
23
|
+
def referral_link
|
24
|
+
"#{Rails.application.routes.url_helpers.root_url}?ref=#{referral_code}"
|
25
|
+
end
|
26
|
+
|
27
|
+
def total_referrals
|
28
|
+
referrals.count
|
29
|
+
end
|
30
|
+
|
31
|
+
def successful_referrals
|
32
|
+
referrals.joins(:referral_box_transactions).distinct.count
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def generate_referral_code
|
38
|
+
return if referral_code.present?
|
39
|
+
|
40
|
+
loop do
|
41
|
+
self.referral_code = SecureRandom.alphanumeric(ReferralBox.configuration.referral_code_length).upcase
|
42
|
+
break unless User.exists?(referral_code: referral_code)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ReferralBox
|
4
|
+
class Configuration
|
5
|
+
attr_accessor :reference_class_name,
|
6
|
+
:earning_rule,
|
7
|
+
:redeem_rule,
|
8
|
+
:tier_thresholds,
|
9
|
+
:reward_modifier,
|
10
|
+
:referral_reward,
|
11
|
+
:on_tier_changed,
|
12
|
+
:points_expiry_days,
|
13
|
+
:referral_code_length,
|
14
|
+
:admin_route_path,
|
15
|
+
:enable_device_tracking,
|
16
|
+
:collect_geo_location
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@reference_class_name = "User"
|
20
|
+
@tier_thresholds = {
|
21
|
+
"Silver" => 500,
|
22
|
+
"Gold" => 1000,
|
23
|
+
"Platinum" => 2500
|
24
|
+
}
|
25
|
+
@points_expiry_days = 90
|
26
|
+
@referral_code_length = 8
|
27
|
+
@admin_route_path = "/referral_box"
|
28
|
+
@enable_device_tracking = true
|
29
|
+
@collect_geo_location = false # Default to false for privacy
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ReferralBox
|
4
|
+
class Engine < ::Rails::Engine
|
5
|
+
isolate_namespace ReferralBox
|
6
|
+
|
7
|
+
config.generators do |g|
|
8
|
+
g.test_framework :rspec
|
9
|
+
g.fixture_replacement :factory_bot
|
10
|
+
g.factory_bot dir: 'spec/factories'
|
11
|
+
end
|
12
|
+
|
13
|
+
initializer "referral_box.assets" do |app|
|
14
|
+
app.config.assets.precompile += %w( referral_box.js )
|
15
|
+
end
|
16
|
+
|
17
|
+
initializer "referral_box.routes" do |app|
|
18
|
+
app.routes.prepend do
|
19
|
+
mount ReferralBox::Engine => ReferralBox.configuration.admin_route_path
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
initializer "referral_box.migrations" do |app|
|
24
|
+
unless app.root.to_s.match root.to_s
|
25
|
+
config.paths["db/migrate"].expanded.each do |expanded_path|
|
26
|
+
app.config.paths["db/migrate"] << expanded_path
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ReferralBox
|
4
|
+
class ReferralLog < ActiveRecord::Base
|
5
|
+
self.table_name = "referral_box_referral_logs"
|
6
|
+
|
7
|
+
belongs_to :referee, polymorphic: true, optional: true
|
8
|
+
|
9
|
+
validates :referral_code, presence: true
|
10
|
+
validates :clicked_at, presence: true
|
11
|
+
|
12
|
+
scope :clicked, -> { where.not(clicked_at: nil) }
|
13
|
+
scope :signed_up, -> { where.not(signed_up_at: nil) }
|
14
|
+
scope :converted, -> { where.not(signed_up_at: nil).where.not(referee_id: nil) }
|
15
|
+
|
16
|
+
def converted?
|
17
|
+
signed_up_at.present? && referee_id.present?
|
18
|
+
end
|
19
|
+
|
20
|
+
def conversion_rate
|
21
|
+
return 0 if clicked_count.zero?
|
22
|
+
(converted_count.to_f / clicked_count * 100).round(2)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.clicked_count
|
26
|
+
clicked.count
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.converted_count
|
30
|
+
converted.count
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.conversion_rate
|
34
|
+
return 0 if clicked_count.zero?
|
35
|
+
(converted_count.to_f / clicked_count * 100).round(2)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Device detection methods - use stored data or fallback to user_agent parsing
|
39
|
+
def device_type
|
40
|
+
return self[:device_type] if self[:device_type].present?
|
41
|
+
return detect_device_type_from_user_agent if user_agent.present?
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def browser
|
46
|
+
return self[:browser] if self[:browser].present?
|
47
|
+
return detect_browser_from_user_agent if user_agent.present?
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
|
51
|
+
# Geo-location methods (if using a geo service)
|
52
|
+
def country
|
53
|
+
return geo_data["country"] if geo_data&.dig("country").present?
|
54
|
+
return nil unless ip_address.present?
|
55
|
+
# You can integrate with services like MaxMind GeoIP2 or similar
|
56
|
+
# For now, return nil - users can implement their own geo service
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def city
|
61
|
+
return geo_data["city"] if geo_data&.dig("city").present?
|
62
|
+
return nil unless ip_address.present?
|
63
|
+
# Implement with your preferred geo service
|
64
|
+
nil
|
65
|
+
end
|
66
|
+
|
67
|
+
def latitude
|
68
|
+
geo_data&.dig("latitude")
|
69
|
+
end
|
70
|
+
|
71
|
+
def longitude
|
72
|
+
geo_data&.dig("longitude")
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def detect_device_type_from_user_agent
|
78
|
+
return nil unless user_agent.present?
|
79
|
+
|
80
|
+
user_agent = user_agent.downcase
|
81
|
+
|
82
|
+
if user_agent.include?("mobile") || user_agent.include?("android") || user_agent.include?("iphone")
|
83
|
+
"mobile"
|
84
|
+
elsif user_agent.include?("tablet") || user_agent.include?("ipad")
|
85
|
+
"tablet"
|
86
|
+
else
|
87
|
+
"desktop"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def detect_browser_from_user_agent
|
92
|
+
return nil unless user_agent.present?
|
93
|
+
|
94
|
+
user_agent = user_agent.downcase
|
95
|
+
|
96
|
+
if user_agent.include?("chrome")
|
97
|
+
"Chrome"
|
98
|
+
elsif user_agent.include?("firefox")
|
99
|
+
"Firefox"
|
100
|
+
elsif user_agent.include?("safari")
|
101
|
+
"Safari"
|
102
|
+
elsif user_agent.include?("edge")
|
103
|
+
"Edge"
|
104
|
+
else
|
105
|
+
"Other"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ReferralBox
|
4
|
+
class Transaction < ActiveRecord::Base
|
5
|
+
self.table_name = "referral_box_transactions"
|
6
|
+
|
7
|
+
belongs_to :user, polymorphic: true
|
8
|
+
|
9
|
+
validates :user, presence: true
|
10
|
+
validates :points, presence: true, numericality: { only_integer: true }
|
11
|
+
validates :transaction_type, presence: true, inclusion: { in: %w[earn redeem adjust] }
|
12
|
+
|
13
|
+
scope :earned, -> { where(transaction_type: "earn") }
|
14
|
+
scope :redeemed, -> { where(transaction_type: "redeem") }
|
15
|
+
scope :adjusted, -> { where(transaction_type: "adjust") }
|
16
|
+
scope :active, -> { where("expires_at IS NULL OR expires_at > ?", Time.current) }
|
17
|
+
scope :expired, -> { where("expires_at IS NOT NULL AND expires_at <= ?", Time.current) }
|
18
|
+
|
19
|
+
def expired?
|
20
|
+
expires_at.present? && expires_at <= Time.current
|
21
|
+
end
|
22
|
+
|
23
|
+
def active?
|
24
|
+
!expired?
|
25
|
+
end
|
26
|
+
|
27
|
+
def earned?
|
28
|
+
transaction_type == "earn"
|
29
|
+
end
|
30
|
+
|
31
|
+
def redeemed?
|
32
|
+
transaction_type == "redeem"
|
33
|
+
end
|
34
|
+
|
35
|
+
def adjusted?
|
36
|
+
transaction_type == "adjust"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|