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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ReferralBox
4
+ VERSION = "0.1.0"
5
+ end