loyalty_ref 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 9121c050f889b1da8ed81d383e140eb59fe2509047624b2bbdee8db73269a61a
4
+ data.tar.gz: 70f184d4e5d77fad62598cf2bdef2acce13d9f1da17771cf53bc992cca402801
5
+ SHA512:
6
+ metadata.gz: b893711e8162a2ba5b4df96a7d6144a73ddb1f504b1e800d719b04dafdbe06a64ab1b3cd611414c4a15bc6e9baf2c81960185395a1249a56aececf769ed4531c
7
+ data.tar.gz: 452d754032c03431816ca66a459e7fa1def567218f87c781aede9d514618d85cd3f19882d59b09aa3e69b6e21dac6cbfbc163c28a336656020c72d5cfa99c0ec
data/CHANGELOG.md ADDED
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2025-01-XX
11
+
12
+ ### Added
13
+ - Initial release of LoyaltyRef gem
14
+ - Points system with customizable earning/redeem rules
15
+ - Dynamic tier levels (Silver, Gold, Platinum)
16
+ - Unique referral codes and multi-level tracking
17
+ - Built-in tracking for signups, geo, device (no Redis needed)
18
+ - Admin dashboard with ERB views
19
+ - Support for any user model (User, Customer, Account, etc.)
20
+ - Rails generator for easy installation
21
+ - Comprehensive configuration system
22
+ - Transaction logging and analytics
23
+ - Referral sharing with full analytics
24
+ - Points expiration system
25
+ - Tier change callbacks
26
+ - Device and browser detection
27
+ - Conversion rate tracking
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Kapil Pal
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,194 @@
1
+ # LoyaltyRef 💎
2
+
3
+ A flexible Ruby gem for building loyalty and referral systems in Rails apps.
4
+
5
+ * 🎁 Reward users with points based on their activity
6
+ * 🧱 Create dynamic tier levels (Silver, Gold, Platinum)
7
+ * 🤝 Add referral rewards with unique referral codes
8
+ * 🔧 Fully customizable and configurable
9
+ * 🎛️ Admin dashboard (ERB based)
10
+ * 🔄 Supports any user model: `User`, `Customer`, `Account`, etc.
11
+
12
+ ---
13
+
14
+ ## 📚 Documentation
15
+
16
+ - **[Quick Start Guide](QUICK_START.md)** - Get running in 5 minutes
17
+ - **[Complete Documentation](DOCUMENTATION.md)** - Full developer guide with examples
18
+ - **[API Reference](DOCUMENTATION.md#api-reference)** - All methods and options
19
+
20
+ ---
21
+
22
+ ## 🚀 Installation
23
+
24
+ ```ruby
25
+ # Gemfile
26
+ gem 'loyalty_ref'
27
+ ```
28
+
29
+ ```bash
30
+ $ bundle install
31
+ $ rails generate loyalty_ref:install
32
+ $ rails db:migrate
33
+ ```
34
+
35
+ ---
36
+
37
+ ## 🛠️ Configuration
38
+
39
+ Create your configuration block in an initializer:
40
+
41
+ ```ruby
42
+ # config/initializers/loyalty_ref.rb
43
+
44
+ LoyaltyRef.configure do |config|
45
+ # Define which model represents your app's user/customer
46
+ config.reference_class_name = 'User' # or 'Customer', 'Account', etc.
47
+
48
+ config.earning_rule = ->(user, event) do
49
+ # Example: earn 10 points per ₹100 spent
50
+ event.amount / 10
51
+ end
52
+
53
+ config.redeem_rule = ->(user, offer) do
54
+ offer.cost_in_points
55
+ end
56
+
57
+ config.tier_thresholds = {
58
+ "Silver" => 500,
59
+ "Gold" => 1000,
60
+ "Platinum" => 2500
61
+ }
62
+
63
+ config.reward_modifier = ->(user) do
64
+ case user.tier
65
+ when "Silver" then 1.0
66
+ when "Gold" then 1.2
67
+ when "Platinum" then 1.5
68
+ else 1.0
69
+ end
70
+ end
71
+
72
+ config.referral_reward = ->(referrer, referee) do
73
+ LoyaltyRef.earn_points(referrer, 100)
74
+ LoyaltyRef.earn_points(referee, 50)
75
+ end
76
+ end
77
+ ```
78
+
79
+ ---
80
+
81
+ ## ✅ Features
82
+
83
+ ### 🎁 Loyalty Program
84
+
85
+ | Feature | Description |
86
+ | -------------------- | ----------------------------- |
87
+ | Points system | Earn points via config lambda |
88
+ | Custom earning rules | Define rules per event/user |
89
+ | Redeem points | Redeem points for offers |
90
+ | Manual adjustment | Admins can modify balances |
91
+ | Points expiration | e.g. 90 days |
92
+ | Transaction logging | All activity is logged |
93
+ | Check balance | Easy method to check |
94
+
95
+ ### 🧱 Tier System (Dynamic)
96
+
97
+ | Feature | Description |
98
+ | ----------------------- | --------------------------- |
99
+ | Dynamic definitions | e.g. Silver => 500 points |
100
+ | Auto tier assignment | Based on balance |
101
+ | Callbacks on promotion | `on_tier_changed` hook |
102
+ | Reward modifier by tier | e.g. Gold users get +20% |
103
+ | DB persistence | Can store or calculate tier |
104
+
105
+ ### 🤝 Referral System
106
+
107
+ | Feature | Description |
108
+ | --------------------- | ------------------------------------------------------------- |
109
+ | Unique referral codes | Auto-generated or custom |
110
+ | ?ref=code tracking | Via signup links |
111
+ | Multi-level referrals | Parent/child tree |
112
+ | Referral rewards | Custom logic supported |
113
+ | Referral analytics | Track clicks, accepted signups, geo-location, and device type |
114
+
115
+ ### ⚙️ Core Gem Features
116
+
117
+ * Developer config block
118
+ * Extensible models
119
+ * Simple public API: `earn_points`, `redeem_points`, `balance`, `track_referral`
120
+ * Rails generators for setup
121
+ * Support for any user model (User, Account, Customer, etc.)
122
+
123
+ ### 🖥️ Admin UI
124
+
125
+ * Mountable engine with ERB templates
126
+ * Routes like `/loyalty`
127
+ * Views to list users, transactions, referrals
128
+
129
+ ---
130
+
131
+ ## 🔮 Future Scope
132
+
133
+ ### 📊 Analytics & Admin
134
+
135
+ * Leaderboards by points
136
+ * Referral tree visualizer
137
+ * ActiveAdmin / custom dashboard
138
+ * Export CSV/JSON of logs
139
+
140
+ ### 🔔 Engagement
141
+
142
+ * Email / in-app notifications
143
+ * Badges based on milestones
144
+ * Activity calendar
145
+ * Social sharing for referral links
146
+
147
+ ---
148
+
149
+ ## 📂 Folder Structure (Gem)
150
+
151
+ ```
152
+ lib/
153
+ ├── loyalty_ref.rb
154
+ ├── loyalty_ref/
155
+ │ ├── engine.rb
156
+ │ ├── configuration.rb
157
+ │ ├── version.rb
158
+ │ ├── models/
159
+ │ │ ├── transaction.rb
160
+ │ │ ├── referral_log.rb
161
+ │ └── controllers/
162
+ │ ├── dashboard_controller.rb
163
+ app/views/loyalty_ref/dashboard/
164
+ ├── index.html.erb
165
+ ├── show.html.erb
166
+ ```
167
+
168
+ ---
169
+
170
+ ## 🧪 Usage Examples
171
+
172
+ ```ruby
173
+ # Earn points
174
+ LoyaltyRef.earn_points(current_user, event: order)
175
+
176
+ # Redeem points
177
+ LoyaltyRef.redeem_points(current_user, offer: coupon)
178
+
179
+ # Check balance
180
+ LoyaltyRef.balance(current_user)
181
+
182
+ # Track referral
183
+ LoyaltyRef.track_referral(ref_code: params[:ref])
184
+ ```
185
+
186
+ ---
187
+
188
+ ## 📬 Contribution
189
+
190
+ PRs are welcome 🙌 — help improve the gem or suggest features.
191
+
192
+ ## 📜 License
193
+
194
+ MIT © 2025 Kapil Pal
@@ -0,0 +1,138 @@
1
+ // LoyaltyRef - Device Detection and Geo-location Collection
2
+ // This script automatically collects device and location data when users click referral links
3
+
4
+ (function() {
5
+ 'use strict';
6
+
7
+ // Check if LoyaltyRef tracking is enabled
8
+ if (typeof window.LoyaltyRefConfig === 'undefined' || !window.LoyaltyRefConfig.enabled) {
9
+ return;
10
+ }
11
+
12
+ // Device detection
13
+ function detectDevice() {
14
+ const userAgent = navigator.userAgent.toLowerCase();
15
+
16
+ // Device type detection
17
+ let deviceType = 'desktop';
18
+ if (/mobile|android|iphone|ipod|blackberry|windows phone/.test(userAgent)) {
19
+ deviceType = 'mobile';
20
+ } else if (/tablet|ipad/.test(userAgent)) {
21
+ deviceType = 'tablet';
22
+ }
23
+
24
+ // Browser detection
25
+ let browser = 'Other';
26
+ if (userAgent.includes('chrome')) {
27
+ browser = 'Chrome';
28
+ } else if (userAgent.includes('firefox')) {
29
+ browser = 'Firefox';
30
+ } else if (userAgent.includes('safari') && !userAgent.includes('chrome')) {
31
+ browser = 'Safari';
32
+ } else if (userAgent.includes('edge')) {
33
+ browser = 'Edge';
34
+ } else if (userAgent.includes('opera')) {
35
+ browser = 'Opera';
36
+ }
37
+
38
+ return {
39
+ device_type: deviceType,
40
+ browser: browser,
41
+ screen_width: screen.width,
42
+ screen_height: screen.height,
43
+ viewport_width: window.innerWidth,
44
+ viewport_height: window.innerHeight,
45
+ language: navigator.language,
46
+ platform: navigator.platform,
47
+ cookie_enabled: navigator.cookieEnabled,
48
+ online: navigator.onLine
49
+ };
50
+ }
51
+
52
+ // Geo-location detection (if enabled and user consents)
53
+ function detectLocation() {
54
+ return new Promise((resolve) => {
55
+ if (!window.LoyaltyRefConfig.collect_geo || !navigator.geolocation) {
56
+ resolve(null);
57
+ return;
58
+ }
59
+
60
+ navigator.geolocation.getCurrentPosition(
61
+ function(position) {
62
+ resolve({
63
+ latitude: position.coords.latitude,
64
+ longitude: position.coords.longitude,
65
+ accuracy: position.coords.accuracy,
66
+ timestamp: position.timestamp
67
+ });
68
+ },
69
+ function(error) {
70
+ console.log('LoyaltyRef: Geolocation not available or denied');
71
+ resolve(null);
72
+ },
73
+ {
74
+ enableHighAccuracy: false,
75
+ timeout: 5000,
76
+ maximumAge: 60000
77
+ }
78
+ );
79
+ });
80
+ }
81
+
82
+ // Collect all device and location data
83
+ async function collectDeviceData() {
84
+ const deviceData = detectDevice();
85
+ const locationData = await detectLocation();
86
+
87
+ return {
88
+ ...deviceData,
89
+ geo_data: locationData,
90
+ collected_at: new Date().toISOString()
91
+ };
92
+ }
93
+
94
+ // Enhanced referral tracking
95
+ function enhanceReferralTracking() {
96
+ // Find all referral links
97
+ const referralLinks = document.querySelectorAll('a[href*="?ref="], a[href*="&ref="]');
98
+
99
+ referralLinks.forEach(link => {
100
+ link.addEventListener('click', async function(e) {
101
+ // Only enhance if it's a referral link
102
+ if (!this.href.includes('ref=')) return;
103
+
104
+ try {
105
+ // Collect device and location data
106
+ const deviceData = await collectDeviceData();
107
+
108
+ // Store in sessionStorage for the next page load
109
+ sessionStorage.setItem('loyalty_ref_device_data', JSON.stringify(deviceData));
110
+
111
+ // Add a small delay to ensure data is stored
112
+ setTimeout(() => {
113
+ // Continue with normal link navigation
114
+ return true;
115
+ }, 100);
116
+
117
+ } catch (error) {
118
+ console.error('LoyaltyRef: Error collecting device data:', error);
119
+ }
120
+ });
121
+ });
122
+ }
123
+
124
+ // Initialize when DOM is ready
125
+ if (document.readyState === 'loading') {
126
+ document.addEventListener('DOMContentLoaded', enhanceReferralTracking);
127
+ } else {
128
+ enhanceReferralTracking();
129
+ }
130
+
131
+ // Make functions available globally for manual use
132
+ window.LoyaltyRef = {
133
+ detectDevice: detectDevice,
134
+ detectLocation: detectLocation,
135
+ collectDeviceData: collectDeviceData
136
+ };
137
+
138
+ })();
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LoyaltyRef
4
+ class DashboardController < ActionController::Base
5
+ layout "loyalty_ref/dashboard"
6
+
7
+ before_action :authenticate_user!
8
+ before_action :ensure_admin_access!
9
+
10
+ def index
11
+ @total_users = user_class.count
12
+ @total_transactions = LoyaltyRef::Transaction.count
13
+ @total_referrals = LoyaltyRef::ReferralLog.count
14
+ @conversion_rate = LoyaltyRef::ReferralLog.conversion_rate
15
+
16
+ @recent_transactions = LoyaltyRef::Transaction.includes(:user)
17
+ .order(created_at: :desc)
18
+ .limit(10)
19
+
20
+ @top_users = user_class.joins(:loyalty_ref_transactions)
21
+ .group("users.id")
22
+ .order("SUM(loyalty_ref_transactions.points) DESC")
23
+ .limit(10)
24
+ end
25
+
26
+ def users
27
+ @users = safe_paginate(user_class.includes(:loyalty_ref_transactions)
28
+ .order(created_at: :desc), 20)
29
+ end
30
+
31
+ def user
32
+ @user = user_class.find(params[:id])
33
+ @transactions = safe_paginate(@user.loyalty_ref_transactions
34
+ .order(created_at: :desc), 20)
35
+ @balance = LoyaltyRef.balance(@user)
36
+ @tier = LoyaltyRef.tier(@user)
37
+ end
38
+
39
+ def transactions
40
+ @transactions = safe_paginate(LoyaltyRef::Transaction.includes(:user)
41
+ .order(created_at: :desc), 50)
42
+ end
43
+
44
+ def referrals
45
+ @referrals = safe_paginate(LoyaltyRef::ReferralLog.order(clicked_at: :desc), 50)
46
+ end
47
+
48
+ def analytics
49
+ @total_clicks = LoyaltyRef::ReferralLog.clicked_count
50
+ @total_conversions = LoyaltyRef::ReferralLog.converted_count
51
+ @conversion_rate = LoyaltyRef::ReferralLog.conversion_rate
52
+
53
+ @referrals_by_device = LoyaltyRef::ReferralLog.group(:device_type).count
54
+ @referrals_by_browser = LoyaltyRef::ReferralLog.group(:browser).count
55
+
56
+ @daily_clicks = LoyaltyRef::ReferralLog.group("DATE(clicked_at)")
57
+ .count
58
+ .sort_by { |date, _| date }
59
+ .last(30)
60
+ end
61
+
62
+ private
63
+
64
+ def user_class
65
+ LoyaltyRef.configuration.reference_class_name.constantize
66
+ end
67
+
68
+ def safe_paginate(relation, per_page = 20)
69
+ if defined?(Kaminari) && relation.respond_to?(:page)
70
+ relation.page(params[:page]).per(per_page)
71
+ else
72
+ # Fallback pagination without Kaminari
73
+ page = (params[:page] || 1).to_i
74
+ offset = (page - 1) * per_page
75
+ relation.offset(offset).limit(per_page)
76
+ end
77
+ end
78
+
79
+ def authenticate_user!
80
+ # Override this method in your app to implement authentication
81
+ # For example: redirect_to login_path unless current_user
82
+ true
83
+ end
84
+
85
+ def ensure_admin_access!
86
+ # Override this method in your app to implement admin authorization
87
+ # For example: redirect_to root_path unless current_user&.admin?
88
+ true
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LoyaltyRef
4
+ class TrackingController < ActionController::Base
5
+ skip_before_action :verify_authenticity_token, only: [:track_referral]
6
+
7
+ def track_referral
8
+ ref_code = params[:ref_code]
9
+ device_data = params[:device_data]
10
+
11
+ if ref_code.present?
12
+ success = LoyaltyRef.track_referral(
13
+ ref_code: ref_code,
14
+ user_agent: request.user_agent,
15
+ ip_address: request.remote_ip,
16
+ referrer: request.referer,
17
+ device_data: device_data
18
+ )
19
+
20
+ if success
21
+ render json: { success: true, message: "Referral tracked successfully" }
22
+ else
23
+ render json: { success: false, message: "Failed to track referral" }, status: 422
24
+ end
25
+ else
26
+ render json: { success: false, message: "Referral code is required" }, status: 400
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LoyaltyRef
4
+ module ApplicationHelper
5
+ # Include LoyaltyRef JavaScript and configuration
6
+ def loyalty_ref_javascript_tag
7
+ return unless LoyaltyRef.configuration.enable_device_tracking
8
+
9
+ config = {
10
+ enabled: LoyaltyRef.configuration.enable_device_tracking,
11
+ collect_geo: LoyaltyRef.configuration.collect_geo_location,
12
+ api_endpoint: loyalty_ref_track_referral_path
13
+ }
14
+
15
+ content_tag(:script, "window.LoyaltyRefConfig = #{config.to_json};", type: 'application/javascript') +
16
+ javascript_include_tag('loyalty_ref', 'data-turbolinks-track': 'reload')
17
+ end
18
+
19
+ # Generate a referral link with enhanced tracking
20
+ def loyalty_ref_referral_link(user, options = {})
21
+ return unless user.respond_to?(:referral_code) && user.referral_code.present?
22
+
23
+ base_url = options[:base_url] || root_url
24
+ ref_param = options[:ref_param] || 'ref'
25
+
26
+ "#{base_url}#{base_url.include?('?') ? '&' : '?'}#{ref_param}=#{user.referral_code}"
27
+ end
28
+
29
+ # Generate a referral link button with styling
30
+ def loyalty_ref_referral_button(user, text = nil, options = {})
31
+ text ||= "Share Referral Link"
32
+ link_text = options[:icon] ? "#{options[:icon]} #{text}" : text
33
+
34
+ link_to(
35
+ link_text,
36
+ loyalty_ref_referral_link(user, options),
37
+ options.merge(
38
+ class: "loyalty-ref-referral-link #{options[:class]}".strip,
39
+ target: options[:target] || '_blank',
40
+ rel: options[:rel] || 'noopener'
41
+ )
42
+ )
43
+ end
44
+
45
+ # Display user's referral stats
46
+ def loyalty_ref_user_stats(user)
47
+ return unless user.respond_to?(:points_balance)
48
+
49
+ content_tag(:div, class: 'loyalty-ref-user-stats') do
50
+ concat(content_tag(:div, "Points: #{user.points_balance}", class: 'points'))
51
+ concat(content_tag(:div, "Tier: #{user.current_tier || 'None'}", class: 'tier'))
52
+ concat(content_tag(:div, "Referrals: #{user.total_referrals rescue 0}", class: 'referrals'))
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module LoyaltyRef
4
+ module DashboardHelper
5
+ def loyalty_paginate(collection)
6
+ if defined?(Kaminari) && collection.respond_to?(:current_page)
7
+ paginate(collection)
8
+ else
9
+ # Simple pagination without Kaminari
10
+ render_simple_pagination(collection)
11
+ end
12
+ end
13
+
14
+ private
15
+
16
+ def render_simple_pagination(collection)
17
+ return unless collection.respond_to?(:limit_value) && collection.respond_to?(:offset_value)
18
+
19
+ current_page = (collection.offset_value / collection.limit_value) + 1
20
+
21
+ # For fallback pagination, we need to get total count differently
22
+ # Since we don't have total_count method, we'll just show basic navigation
23
+ content_tag(:div, class: 'pagination') do
24
+ links = []
25
+
26
+ # Previous page
27
+ if current_page > 1
28
+ links << link_to('Previous', url_for(page: current_page - 1), class: 'pagination-link')
29
+ end
30
+
31
+ # Current page
32
+ links << content_tag(:span, "Page #{current_page}", class: 'pagination-current')
33
+
34
+ # Next page (we'll always show this for fallback)
35
+ links << link_to('Next', url_for(page: current_page + 1), class: 'pagination-link')
36
+
37
+ links.join.html_safe
38
+ end
39
+ end
40
+ end
41
+ end