app_manager 2.4.11 → 2.7.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 +4 -4
- data/Gemfile.lock +29 -24
- data/app/controllers/app_manager/charges_controller.rb +21 -6
- data/app/controllers/app_manager/plans_controller.rb +46 -3
- data/app/model/app_manager/app_faq.rb +5 -0
- data/lib/app_manager/client/connection.rb +2 -0
- data/lib/app_manager/client/plans.rb +4 -0
- data/lib/app_manager/fail_safe.rb +216 -45
- data/lib/app_manager/graphql_helper.rb +47 -49
- data/lib/app_manager/version.rb +1 -1
- data/lib/app_manager.rb +6 -0
- data/lib/generators/app_manager/install/install_generator.rb +24 -0
- data/lib/generators/app_manager/install/templates/add_charge_discount_snapshot_and_used.erb +12 -0
- data/lib/generators/app_manager/install/templates/add_trial_days_text_most_popular_plan_badge_to_plans.erb +7 -0
- data/lib/generators/app_manager/install/templates/create_app_faqs_table.erb +14 -0
- data/lib/generators/app_manager/install/templates/failsafe_tables.erb +7 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b07aa041091f2f5c6acc9fc85b9ffbf6c73d7d97f0694981fb3ee261fd4d167c
|
|
4
|
+
data.tar.gz: 3ef460faa396f6dbb9df6cb66010045975cd76b28a29e97d00b4542bb846a0c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 22f3bb93b50b5064d8db803023882e48539e9c212703b922379378e3cff63d017020b96c7cf3d2bdf3402768e4011f1ffbbd7ba823d513c3d81c9881a6f91a69
|
|
7
|
+
data.tar.gz: 423fbd2cea4724aaeeacfc86fc6f90d15df211fc8a74a23c8e6ac2c93ff3744d27c502873d55b3beb03d3783db3492d74b26c63f2d0d940209668715bccbc8d1
|
data/Gemfile.lock
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
app_manager (2.
|
|
4
|
+
app_manager (2.7.0)
|
|
5
5
|
activerecord-import (~> 1.4)
|
|
6
6
|
httparty
|
|
7
7
|
kaminari (>= 0.16.3)
|
|
@@ -10,7 +10,7 @@ PATH
|
|
|
10
10
|
GEM
|
|
11
11
|
remote: https://rubygems.org/
|
|
12
12
|
specs:
|
|
13
|
-
action_text-trix (2.1.
|
|
13
|
+
action_text-trix (2.1.19)
|
|
14
14
|
railties
|
|
15
15
|
actioncable (8.1.3)
|
|
16
16
|
actionpack (= 8.1.3)
|
|
@@ -92,27 +92,27 @@ GEM
|
|
|
92
92
|
bigdecimal (4.0.1)
|
|
93
93
|
builder (3.3.0)
|
|
94
94
|
coderay (1.1.3)
|
|
95
|
-
concurrent-ruby (1.3.
|
|
95
|
+
concurrent-ruby (1.3.8)
|
|
96
96
|
connection_pool (3.0.2)
|
|
97
97
|
crack (1.0.1)
|
|
98
98
|
bigdecimal
|
|
99
99
|
rexml
|
|
100
|
-
crass (1.0.
|
|
100
|
+
crass (1.0.7)
|
|
101
101
|
csv (3.3.5)
|
|
102
102
|
date (3.5.1)
|
|
103
103
|
diff-lcs (1.6.2)
|
|
104
104
|
dotenv (2.1.2)
|
|
105
105
|
drb (2.2.3)
|
|
106
|
-
erb (6.0.
|
|
106
|
+
erb (6.0.6)
|
|
107
107
|
erubi (1.13.1)
|
|
108
|
-
globalid (1.
|
|
108
|
+
globalid (1.4.0)
|
|
109
109
|
activesupport (>= 6.1)
|
|
110
110
|
hashdiff (1.2.1)
|
|
111
111
|
httparty (0.24.2)
|
|
112
112
|
csv
|
|
113
113
|
mini_mime (>= 1.0.0)
|
|
114
114
|
multi_xml (>= 0.5.2)
|
|
115
|
-
i18n (1.
|
|
115
|
+
i18n (1.15.2)
|
|
116
116
|
concurrent-ruby (~> 1.0)
|
|
117
117
|
io-console (0.8.2)
|
|
118
118
|
irb (1.18.0)
|
|
@@ -120,7 +120,7 @@ GEM
|
|
|
120
120
|
prism (>= 1.3.0)
|
|
121
121
|
rdoc (>= 4.0.0)
|
|
122
122
|
reline (>= 0.4.2)
|
|
123
|
-
json (2.
|
|
123
|
+
json (2.21.1)
|
|
124
124
|
kaminari (1.2.2)
|
|
125
125
|
activesupport (>= 4.1.0)
|
|
126
126
|
kaminari-actionview (= 1.2.2)
|
|
@@ -134,24 +134,25 @@ GEM
|
|
|
134
134
|
kaminari-core (= 1.2.2)
|
|
135
135
|
kaminari-core (1.2.2)
|
|
136
136
|
logger (1.7.0)
|
|
137
|
-
loofah (2.25.
|
|
137
|
+
loofah (2.25.2)
|
|
138
138
|
crass (~> 1.0.2)
|
|
139
139
|
nokogiri (>= 1.12.0)
|
|
140
|
-
mail (2.9.
|
|
140
|
+
mail (2.9.1)
|
|
141
141
|
logger
|
|
142
142
|
mini_mime (>= 0.1.1)
|
|
143
143
|
net-imap
|
|
144
144
|
net-pop
|
|
145
145
|
net-smtp
|
|
146
|
-
marcel (1.1
|
|
146
|
+
marcel (1.2.1)
|
|
147
147
|
method_source (1.1.0)
|
|
148
148
|
mini_mime (1.1.5)
|
|
149
|
+
mini_portile2 (2.8.9)
|
|
149
150
|
minitest (6.0.6)
|
|
150
151
|
drb (~> 2.0)
|
|
151
152
|
prism (~> 1.5)
|
|
152
153
|
multi_xml (0.9.1)
|
|
153
154
|
bigdecimal (>= 3.1, < 5)
|
|
154
|
-
net-imap (0.6.4)
|
|
155
|
+
net-imap (0.6.4.1)
|
|
155
156
|
date
|
|
156
157
|
net-protocol
|
|
157
158
|
net-pop (0.1.2)
|
|
@@ -161,9 +162,12 @@ GEM
|
|
|
161
162
|
net-smtp (0.5.1)
|
|
162
163
|
net-protocol
|
|
163
164
|
nio4r (2.7.5)
|
|
164
|
-
nokogiri (1.19.
|
|
165
|
+
nokogiri (1.19.4)
|
|
166
|
+
mini_portile2 (~> 2.8.2)
|
|
165
167
|
racc (~> 1.4)
|
|
166
|
-
|
|
168
|
+
nokogiri (1.19.4-x86_64-linux-gnu)
|
|
169
|
+
racc (~> 1.4)
|
|
170
|
+
pp (0.6.4)
|
|
167
171
|
prettyprint
|
|
168
172
|
prettyprint (0.2.0)
|
|
169
173
|
prism (1.9.0)
|
|
@@ -171,9 +175,6 @@ GEM
|
|
|
171
175
|
coderay (~> 1.1)
|
|
172
176
|
method_source (~> 1.0)
|
|
173
177
|
reline (>= 0.6.0)
|
|
174
|
-
psych (5.3.1)
|
|
175
|
-
date
|
|
176
|
-
stringio
|
|
177
178
|
public_suffix (7.0.2)
|
|
178
179
|
racc (1.8.1)
|
|
179
180
|
rack (3.2.6)
|
|
@@ -202,8 +203,8 @@ GEM
|
|
|
202
203
|
activesupport (>= 5.0.0)
|
|
203
204
|
minitest
|
|
204
205
|
nokogiri (>= 1.6)
|
|
205
|
-
rails-html-sanitizer (1.7.
|
|
206
|
-
loofah (~> 2.25)
|
|
206
|
+
rails-html-sanitizer (1.7.1)
|
|
207
|
+
loofah (~> 2.25, >= 2.25.2)
|
|
207
208
|
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
|
|
208
209
|
railties (8.1.3)
|
|
209
210
|
actionpack (= 8.1.3)
|
|
@@ -215,9 +216,14 @@ GEM
|
|
|
215
216
|
tsort (>= 0.2)
|
|
216
217
|
zeitwerk (~> 2.6)
|
|
217
218
|
rake (13.3.1)
|
|
218
|
-
|
|
219
|
+
rbs (4.0.3)
|
|
220
|
+
logger
|
|
221
|
+
prism (>= 1.6.0)
|
|
222
|
+
tsort
|
|
223
|
+
rdoc (8.0.0)
|
|
219
224
|
erb
|
|
220
|
-
|
|
225
|
+
prism (>= 1.6.0)
|
|
226
|
+
rbs (>= 4.0.0)
|
|
221
227
|
tsort
|
|
222
228
|
reline (0.6.3)
|
|
223
229
|
io-console (~> 0.5)
|
|
@@ -238,7 +244,6 @@ GEM
|
|
|
238
244
|
rspec-support (~> 3.13.0)
|
|
239
245
|
rspec-support (3.13.7)
|
|
240
246
|
securerandom (0.4.1)
|
|
241
|
-
stringio (3.2.0)
|
|
242
247
|
thor (1.5.0)
|
|
243
248
|
timeout (0.6.1)
|
|
244
249
|
tsort (0.2.0)
|
|
@@ -250,11 +255,11 @@ GEM
|
|
|
250
255
|
addressable (>= 2.8.0)
|
|
251
256
|
crack (>= 0.3.2)
|
|
252
257
|
hashdiff (>= 0.4.0, < 2.0.0)
|
|
253
|
-
websocket-driver (0.8.
|
|
258
|
+
websocket-driver (0.8.2)
|
|
254
259
|
base64
|
|
255
260
|
websocket-extensions (>= 0.1.0)
|
|
256
261
|
websocket-extensions (0.1.5)
|
|
257
|
-
zeitwerk (2.
|
|
262
|
+
zeitwerk (2.8.2)
|
|
258
263
|
|
|
259
264
|
PLATFORMS
|
|
260
265
|
ruby
|
|
@@ -34,7 +34,7 @@ module AppManager
|
|
|
34
34
|
Rollbar.error("APP MANAGER Process Plan Failed #{e.inspect}-#{e.backtrace}")
|
|
35
35
|
end
|
|
36
36
|
@trial_activated_field = AppManager.configuration.field_names['trial_activated_at']
|
|
37
|
-
update_info = {@plan_field => params[:plan_id],
|
|
37
|
+
update_info = {@plan_field => params[:plan_id], grandfathered_field => 0}
|
|
38
38
|
if !config_trial_days.nil? && !plan_data.nil?
|
|
39
39
|
trial_days = plan_data['trial_days'] || 0
|
|
40
40
|
update_info[config_trial_days] = trial_days
|
|
@@ -58,7 +58,8 @@ module AppManager
|
|
|
58
58
|
raise Error, "Invalid charge"
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
|
-
|
|
61
|
+
charge_timestamp = Time.now.to_i
|
|
62
|
+
request_data = {'shop' => @shop.shopify_domain, 'timestamp' => charge_timestamp, 'plan' => params[:plan_id]}
|
|
62
63
|
request_data.merge!('host' => params['host']) if params['host'].present?
|
|
63
64
|
request_data.merge!('old_plan' => params['old_plan']) if params['old_plan'].present?
|
|
64
65
|
|
|
@@ -66,6 +67,14 @@ module AppManager
|
|
|
66
67
|
gq_obj = AppManager::GraphqlHelper.new(@shop.shopify_domain, @shop.shopify_token)
|
|
67
68
|
discount_local_storage = params[:discount_code].present? && !params[:discount_code].nil? ? params[:discount_code] : nil
|
|
68
69
|
data = gq_obj.recurring_charge_api_call(plan_data, return_url, @shop,discount_local_storage)
|
|
70
|
+
# Stash the discount snapshot so the callback can persist it on the charge row.
|
|
71
|
+
if gq_obj.discount_snapshot.present?
|
|
72
|
+
begin
|
|
73
|
+
Rails.cache.write(AppManager.charge_snapshot_cache_key(@shop.shopify_domain, charge_timestamp), gq_obj.discount_snapshot, expires_in: 1.hour)
|
|
74
|
+
rescue Exception => e
|
|
75
|
+
Rollbar.error("APP MANAGER Failed to cache discount snapshot #{e.inspect}")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
69
78
|
if data.present? && !data["errors"].present? && (data["data"].present? && data["data"]["appSubscriptionCreate"].present? && (!data["data"]["appSubscriptionCreate"]["userErrors"].any? && data["data"]["appSubscriptionCreate"]["confirmationUrl"]))
|
|
70
79
|
redirect_charge = data["data"]["appSubscriptionCreate"]["confirmationUrl"]
|
|
71
80
|
render json: {'redirect_url' => redirect_charge}
|
|
@@ -111,6 +120,14 @@ module AppManager
|
|
|
111
120
|
|
|
112
121
|
['api_client_id', 'return_url', 'decorated_return_url','id','id','currency'].each { |k| charge.delete k }
|
|
113
122
|
|
|
123
|
+
# Merge the discount snapshot captured at charge creation onto the charge row.
|
|
124
|
+
snapshot_key = AppManager.charge_snapshot_cache_key(params[:shop], params[:timestamp])
|
|
125
|
+
discount_snapshot = Rails.cache.read(snapshot_key) rescue nil
|
|
126
|
+
if discount_snapshot.is_a?(Hash)
|
|
127
|
+
discount_snapshot.each { |field, value| charge[field] = value unless value.nil? || value == '' }
|
|
128
|
+
Rails.cache.delete(snapshot_key) rescue nil
|
|
129
|
+
end
|
|
130
|
+
|
|
114
131
|
charge_ob = AppManager::Client.new(nil, json_req = true)
|
|
115
132
|
response = charge_ob.store_charge(charge.to_json)
|
|
116
133
|
|
|
@@ -209,11 +226,10 @@ module AppManager
|
|
|
209
226
|
if params[:shop].present? && params[:plan_id].present?
|
|
210
227
|
@shop = shop_data
|
|
211
228
|
grandfathered_field = @field_names['grandfathered']
|
|
212
|
-
trial_activated_field = @field_names['trial_activated_at']
|
|
213
229
|
if !@shop.nil?
|
|
214
230
|
plan_obj = AppManager::Client.new
|
|
215
231
|
plan_data = plan_obj.get_plan(params[:plan_id], params[:shop])
|
|
216
|
-
update_info = {@plan_field => params[:plan_id],
|
|
232
|
+
update_info = {@plan_field => params[:plan_id], grandfathered_field => 0}
|
|
217
233
|
if !config_trial_days.nil? && !plan_data.nil?
|
|
218
234
|
trial_days = plan_data['trial_days'] || 0
|
|
219
235
|
update_info[config_trial_days] = trial_days
|
|
@@ -242,9 +258,8 @@ module AppManager
|
|
|
242
258
|
if params[:shop].present?
|
|
243
259
|
@shop = shop_data
|
|
244
260
|
grandfathered_field = @field_names['grandfathered']
|
|
245
|
-
trial_activated_field = @field_names['trial_activated_at']
|
|
246
261
|
if !@shop.nil?
|
|
247
|
-
update_info = {@plan_field => nil,
|
|
262
|
+
update_info = {@plan_field => nil, grandfathered_field => 0}
|
|
248
263
|
if !config_trial_days.nil?
|
|
249
264
|
update_info[config_trial_days] = 0
|
|
250
265
|
end
|
|
@@ -95,6 +95,33 @@ module AppManager
|
|
|
95
95
|
bundle_obj = AppManager::Client.new
|
|
96
96
|
app_bundle_data = bundle_obj.get_app_bundle_data
|
|
97
97
|
|
|
98
|
+
app_faqs_obj = AppManager::Client.new
|
|
99
|
+
app_faqs = app_faqs_obj.get_app_faqs || []
|
|
100
|
+
|
|
101
|
+
# Embed the active charge's effective pricing onto the matching plan(s), so it
|
|
102
|
+
# travels with the plan payload the frontend already consumes.
|
|
103
|
+
active_charge_pricing = nil
|
|
104
|
+
if active_charge.is_a?(Hash) && active_charge['active_charge'].is_a?(Hash) && active_charge['active_charge']['effective_price']
|
|
105
|
+
ac = active_charge['active_charge']
|
|
106
|
+
active_charge_pricing = {
|
|
107
|
+
'plan_id' => ac['plan_id'],
|
|
108
|
+
'interval' => ac['interval'],
|
|
109
|
+
'effective_price' => ac['effective_price'],
|
|
110
|
+
'strike_price' => ac['strike_price'],
|
|
111
|
+
'is_discount_active' => ac['is_discount_active'] || false,
|
|
112
|
+
'discount_ends_on' => ac['discount_ends_on'],
|
|
113
|
+
'remaining_intervals' => ac['remaining_intervals'],
|
|
114
|
+
'discount_value' => ac['discount_value'],
|
|
115
|
+
'discount_type' => ac['discount_type'],
|
|
116
|
+
'discount_duration_intervals' => ac['discount_duration_intervals']
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
if active_charge_pricing.present?
|
|
121
|
+
plans = plans.map { |p| with_active_charge_pricing(p, active_charge_pricing) } if plans.is_a?(Array)
|
|
122
|
+
plan = with_active_charge_pricing(plan, active_charge_pricing)
|
|
123
|
+
end
|
|
124
|
+
|
|
98
125
|
response = {
|
|
99
126
|
'plans' => plans,
|
|
100
127
|
'promotional_discount' => promotional_discount,
|
|
@@ -105,12 +132,24 @@ module AppManager
|
|
|
105
132
|
'default_plan_id' => default_plan_id,
|
|
106
133
|
'most_popular_plan_ids' => most_popular_plan_ids,
|
|
107
134
|
'choose_later' => choose_later,
|
|
108
|
-
'has_active_charge' => (((active_charge && active_charge.any? && !active_charge['active_charge'].nil?)
|
|
109
|
-
'global_plan_charge' => plan.present? && plan['is_global'] && active_charge.present? && active_charge.any? && !active_charge['active_charge'].nil? && !active_charge['bundle_charge'].nil
|
|
135
|
+
'has_active_charge' => (((active_charge && active_charge.any? && !active_charge['active_charge'].nil?) || (plan.present? && plan['price'].to_f == 0) || !trial_activated_at) ? true : false),
|
|
136
|
+
'global_plan_charge' => plan.present? && plan['is_global'] && active_charge.present? && active_charge.any? && !active_charge['active_charge'].nil? && !active_charge['bundle_charge'].nil?,
|
|
137
|
+
'app_faqs' => app_faqs
|
|
110
138
|
}
|
|
111
139
|
render json: response
|
|
112
140
|
end
|
|
113
141
|
|
|
142
|
+
# Attaches active_charge_pricing to a plan only when it is the plan the active
|
|
143
|
+
# charge belongs to (same id and, when known, same interval).
|
|
144
|
+
def with_active_charge_pricing(plan, pricing)
|
|
145
|
+
return plan unless plan.is_a?(Hash) && plan.present?
|
|
146
|
+
matches_plan = plan['id'].to_s == pricing['plan_id'].to_s
|
|
147
|
+
plan_interval = plan['interval'].is_a?(Hash) ? plan['interval']['value'] : plan['interval']
|
|
148
|
+
matches_interval = pricing['interval'].blank? || plan_interval == pricing['interval']
|
|
149
|
+
plan = plan.merge('active_charge_pricing' => pricing) if matches_plan && matches_interval
|
|
150
|
+
plan
|
|
151
|
+
end
|
|
152
|
+
|
|
114
153
|
def users
|
|
115
154
|
if model
|
|
116
155
|
|
|
@@ -166,7 +205,11 @@ module AppManager
|
|
|
166
205
|
@trial_activated_field = AppManager.configuration.field_names['trial_activated_at']
|
|
167
206
|
plan_obj = AppManager::Client.new
|
|
168
207
|
plan_data = plan_obj.get_plan(params[:plan_id], params[:shop_domain]) rescue nil
|
|
169
|
-
|
|
208
|
+
# Only stamp the trial start the first time; re-assigning a no-charge plan must
|
|
209
|
+
# not restart the trial clock (that would hand back trial days already consumed).
|
|
210
|
+
existing_trial_activated_at = @shop[@trial_activated_field] rescue nil
|
|
211
|
+
update_info = {@plan_field => params[:plan_id]}
|
|
212
|
+
update_info[@trial_activated_field] = DateTime.now if existing_trial_activated_at.blank?
|
|
170
213
|
if !config_trial_days.nil? && !plan_data.nil?
|
|
171
214
|
trial_days = plan_data['trial_days'] || 0
|
|
172
215
|
update_info[config_trial_days] = trial_days
|
|
@@ -133,6 +133,8 @@ module AppManager
|
|
|
133
133
|
return @fs.store_discount_used(params,options)
|
|
134
134
|
when 'get-related-discounted-plans'
|
|
135
135
|
return @fs.get_local_related_discounted_plans(params,options)
|
|
136
|
+
when 'app-faqs'
|
|
137
|
+
return @fs.get_local_app_faqs
|
|
136
138
|
when 'app-bundle-data'
|
|
137
139
|
return []
|
|
138
140
|
when 'activate-global-plan'
|
|
@@ -87,6 +87,12 @@ module AppManager
|
|
|
87
87
|
Rollbar.error("[Promotional discounts usage log] APP MANAGER >>>> #{e.inspect}")
|
|
88
88
|
end
|
|
89
89
|
|
|
90
|
+
begin
|
|
91
|
+
save_api_app_faqs(params["app_faqs"])
|
|
92
|
+
rescue Exception => e
|
|
93
|
+
Rollbar.error("[app_faqs] APP MANAGER >>>> #{e.inspect}")
|
|
94
|
+
end
|
|
95
|
+
|
|
90
96
|
end
|
|
91
97
|
|
|
92
98
|
def save_api_plans(plans)
|
|
@@ -130,6 +136,9 @@ module AppManager
|
|
|
130
136
|
plan_columns = AppManager::Plan.column_names
|
|
131
137
|
plan_attrs[:details] = details if plan_columns.include?('details')
|
|
132
138
|
plan_attrs[:note] = note if plan_columns.include?('note')
|
|
139
|
+
plan_attrs[:trial_days_text] = plan["trial_days_text"] if plan_columns.include?('trial_days_text')
|
|
140
|
+
plan_attrs[:most_popular] = plan["most_popular"] if plan_columns.include?('most_popular')
|
|
141
|
+
plan_attrs[:plan_badge] = plan["plan_badge"] if plan_columns.include?('plan_badge')
|
|
133
142
|
plan_data << AppManager::Plan.new(plan_attrs)
|
|
134
143
|
end
|
|
135
144
|
end
|
|
@@ -150,13 +159,26 @@ module AppManager
|
|
|
150
159
|
charges.each do |charge|
|
|
151
160
|
charge_test = charge['test'] ? true : false
|
|
152
161
|
if !charge["id"].nil?
|
|
153
|
-
|
|
162
|
+
attrs = { c_id: charge["id"], charge_id: charge["charge_id"], test: charge_test, status: charge["status"], name: charge["name"], type: charge["type"], price: charge["price"], interval: charge["interval"], trial_days: charge["trial_days"], billing_on: charge["billing_on"], activated_on: charge["activated_on"], trial_ends_on: charge["trial_ends_on"], cancelled_on: charge["cancelled_on"], expires_on: charge["expires_on"], plan_id: charge["plan_id"], description: charge["description"], shop_domain: charge["shop_domain"], created_at: charge["created_at"], updated_at: charge["updated_at"], app_id: charge["app_id"], sync: true }
|
|
163
|
+
add_charge_discount_snapshot(attrs, charge)
|
|
164
|
+
charge_data << AppManager::Charge.new(attrs)
|
|
154
165
|
end
|
|
155
166
|
end
|
|
156
167
|
AppManager::Charge.bulk_import charge_data
|
|
157
168
|
end
|
|
158
169
|
end
|
|
159
170
|
|
|
171
|
+
# Copies the discount snapshot fields (value/type/duration/source/promo id) from a
|
|
172
|
+
# charge payload onto the failsafe attrs, but only for columns that actually exist
|
|
173
|
+
# so older (un-migrated) installs keep working.
|
|
174
|
+
def add_charge_discount_snapshot(attrs, charge)
|
|
175
|
+
columns = AppManager::Charge.column_names
|
|
176
|
+
%w[discount_value discount_type discount_duration_intervals discount_source promotional_discount_id].each do |field|
|
|
177
|
+
attrs[field.to_sym] = charge[field] if columns.include?(field) && !charge[field].nil?
|
|
178
|
+
end
|
|
179
|
+
attrs
|
|
180
|
+
end
|
|
181
|
+
|
|
160
182
|
|
|
161
183
|
def save_api_apps(apps)
|
|
162
184
|
begin
|
|
@@ -195,8 +217,11 @@ module AppManager
|
|
|
195
217
|
end
|
|
196
218
|
if discount_plans.any?
|
|
197
219
|
discount_plans_data = []
|
|
220
|
+
has_used_column = AppManager::DiscountPlan.column_names.include?('used')
|
|
198
221
|
discount_plans.each do |discount_plan|
|
|
199
|
-
|
|
222
|
+
attrs = { discount_plan_id: discount_plan['id'], discount: discount_plan['discount'], shop_domain: discount_plan['shop_domain'], cycle_count: discount_plan['cycle_count'], plan_id: discount_plan['plan_id'], created_by: discount_plan['created_by'], created_at: discount_plan['created_at'], updated_at: discount_plan['updated_at'], app_id: discount_plan['app_id'], discount_type: discount_plan['discount_type'] }
|
|
223
|
+
attrs[:used] = discount_plan['used'] ? true : false if has_used_column
|
|
224
|
+
discount_plans_data << AppManager::DiscountPlan.new(attrs)
|
|
200
225
|
# discount_plans_data << AppManager::DiscountPlan.new(id: discount_plan['id'], discount: discount_plan['discount'], shop_domain: discount_plan['shop_domain'], cycle_count: discount_plan['cycle_count'], plan_id: discount_plan['plan_id'], created_by: discount_plan['created_by'], created_at: discount_plan['created_at'], updated_at: discount_plan['updated_at'], app_id: discount_plan['app_id'], discount_type: discount_plan['discount_type'])
|
|
201
226
|
end
|
|
202
227
|
AppManager::DiscountPlan.bulk_import discount_plans_data
|
|
@@ -229,8 +254,11 @@ module AppManager
|
|
|
229
254
|
end
|
|
230
255
|
if plan_users.any?
|
|
231
256
|
extend_plan_users = []
|
|
257
|
+
has_used_column = AppManager::PlanUser.column_names.include?('used')
|
|
232
258
|
plan_users.each do |plan_user|
|
|
233
|
-
|
|
259
|
+
attrs = { plan_user_id: plan_user['id'], shop_domain: plan_user['shop_domain'], plan_id: plan_user['plan_id'], app_id: plan_user['app_id'], created_by: plan_user['created_by'], created_at: plan_user['created_at'], updated_at: plan_user['updated_at'] }
|
|
260
|
+
attrs[:used] = plan_user['used'] ? true : false if has_used_column
|
|
261
|
+
extend_plan_users << AppManager::PlanUser.new(attrs)
|
|
234
262
|
# extend_plan_users << AppManager::PlanUser.new(id: plan_user['id'], shop_domain: plan_user['shop_domain'], plan_id: plan_user['plan_id'], created_by: plan_user['created_by'], created_at: plan_user['created_at'], updated_at: plan_user['updated_at'])
|
|
235
263
|
end
|
|
236
264
|
AppManager::PlanUser.bulk_import extend_plan_users
|
|
@@ -336,6 +364,29 @@ module AppManager
|
|
|
336
364
|
end
|
|
337
365
|
end
|
|
338
366
|
|
|
367
|
+
def save_api_app_faqs(app_faqs)
|
|
368
|
+
begin
|
|
369
|
+
AppManager::AppFaq.connection.truncate(AppManager::AppFaq.table_name)
|
|
370
|
+
rescue
|
|
371
|
+
AppManager::AppFaq.delete_all
|
|
372
|
+
end
|
|
373
|
+
return unless app_faqs&.any?
|
|
374
|
+
app_faqs_data = app_faqs.map do |faq|
|
|
375
|
+
AppManager::AppFaq.new(
|
|
376
|
+
question: faq['question'],
|
|
377
|
+
answer: faq['answer'],
|
|
378
|
+
sort_order: faq['sort_order'] || 0,
|
|
379
|
+
created_at: faq['created_at'],
|
|
380
|
+
updated_at: faq['updated_at']
|
|
381
|
+
)
|
|
382
|
+
end
|
|
383
|
+
AppManager::AppFaq.bulk_import app_faqs_data
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def get_local_app_faqs
|
|
387
|
+
AppManager::AppFaq.order(:sort_order, :id).map(&:attributes)
|
|
388
|
+
end
|
|
389
|
+
|
|
339
390
|
def get_local_app_structures
|
|
340
391
|
app_structure = AppManager::AppStructure.first
|
|
341
392
|
app_structure_computed = {}
|
|
@@ -364,6 +415,7 @@ module AppManager
|
|
|
364
415
|
|
|
365
416
|
custom_plan_ids = []
|
|
366
417
|
plan_users = AppManager::PlanUser.where(shop_domain: params['shop_domain'])
|
|
418
|
+
plan_users = plan_users.where(used: false) if AppManager::PlanUser.column_names.include?('used')
|
|
367
419
|
custom_plan_ids = plan_users.pluck(:plan_id) if plan_users.present?
|
|
368
420
|
|
|
369
421
|
custom_plan_base_ids = []
|
|
@@ -442,6 +494,7 @@ module AppManager
|
|
|
442
494
|
custom_discounts_data = []
|
|
443
495
|
if params["shop_domain"].present? && plan_data
|
|
444
496
|
custom_discounts = AppManager::DiscountPlan.where(shop_domain: params['shop_domain']).order(created_at: :desc)
|
|
497
|
+
custom_discounts = custom_discounts.where(used: false) if AppManager::DiscountPlan.column_names.include?('used')
|
|
445
498
|
if custom_discounts.present?
|
|
446
499
|
custom_discounts.each do |custom_discount|
|
|
447
500
|
new_custom_discount = {}
|
|
@@ -464,6 +517,7 @@ module AppManager
|
|
|
464
517
|
plan['discount'] = cd['discount']
|
|
465
518
|
plan['discount_type'] = cd['discount_type']
|
|
466
519
|
plan['cycle_count'] = cd['cycle_count']
|
|
520
|
+
plan['discount_is_custom'] = true
|
|
467
521
|
end
|
|
468
522
|
end
|
|
469
523
|
|
|
@@ -524,10 +578,12 @@ module AppManager
|
|
|
524
578
|
plan_data['old_plan_id'] = nil # Temporary for migration puspose only
|
|
525
579
|
if params["shop_domain"].present? && plan_data
|
|
526
580
|
discount_plans = AppManager::DiscountPlan.where(plan_id: params["plan_id"], shop_domain: params["shop_domain"])
|
|
581
|
+
discount_plans = discount_plans.where(used: false) if AppManager::DiscountPlan.column_names.include?('used')
|
|
527
582
|
discount_plans.each do |cd|
|
|
528
583
|
plan_data['discount'] = cd['discount'] if cd rescue plan_data['discount']
|
|
529
584
|
plan_data['discount_type'] = cd['discount_type'] if cd rescue plan_data['discount_type']
|
|
530
585
|
plan_data['cycle_count'] = cd['cycle_count'] if cd rescue plan_data['cycle_count']
|
|
586
|
+
plan_data['discount_is_custom'] = true if cd
|
|
531
587
|
end
|
|
532
588
|
end
|
|
533
589
|
|
|
@@ -540,54 +596,82 @@ module AppManager
|
|
|
540
596
|
end
|
|
541
597
|
|
|
542
598
|
def get_local_remaining_days(params, options)
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
599
|
+
shop_domain = params['shop_domain']
|
|
600
|
+
plan_id = params['plan_id']
|
|
601
|
+
trial_activated_at = params['trial_activated_at']
|
|
602
|
+
|
|
603
|
+
return nil if plan_id.blank?
|
|
604
|
+
|
|
605
|
+
plan_trial_days = (AppManager::Plan.where(plan_id: plan_id).limit(1).pluck(:trial_days).first || 0).to_i
|
|
606
|
+
|
|
607
|
+
# Extension stack - sum every record for this plan.
|
|
608
|
+
extension_days = AppManager::ExtendTrial.where(shop_domain: shop_domain, plan_id: plan_id).sum(:days).to_i
|
|
609
|
+
|
|
610
|
+
consumed = prepare_consumed_trial_days(shop_domain, trial_activated_at)
|
|
611
|
+
|
|
612
|
+
[0, (plan_trial_days + extension_days) - consumed].max
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
# Offline mirror of the portal's consumed-trial-days calculation. Total trial days
|
|
616
|
+
# already consumed, excluding time spent on a free plan (paused via
|
|
617
|
+
# charge.cancelled_on). Derived from existing failsafe tables only.
|
|
618
|
+
def prepare_consumed_trial_days(shop_domain, trial_activated_at = nil)
|
|
619
|
+
windows = []
|
|
620
|
+
earliest_charge_start = nil
|
|
621
|
+
|
|
622
|
+
charges = AppManager::Charge.where(shop_domain: shop_domain)
|
|
623
|
+
.where('trial_days > 0')
|
|
624
|
+
.where.not(trial_ends_on: nil)
|
|
625
|
+
|
|
626
|
+
charges.each do |charge|
|
|
627
|
+
trial_end = to_datetime_safe(charge['trial_ends_on'])
|
|
628
|
+
next if trial_end.nil?
|
|
629
|
+
start = trial_end - charge['trial_days'].to_i.days
|
|
630
|
+
|
|
631
|
+
earliest_charge_start = start if earliest_charge_start.nil? || start < earliest_charge_start
|
|
632
|
+
|
|
633
|
+
finish = DateTime.now
|
|
634
|
+
if charge['cancelled_on'].present?
|
|
635
|
+
cancelled_on = to_datetime_safe(charge['cancelled_on'])
|
|
636
|
+
finish = cancelled_on if cancelled_on && cancelled_on < finish
|
|
564
637
|
end
|
|
565
|
-
|
|
638
|
+
finish = trial_end if trial_end < finish
|
|
639
|
+
windows << [start, finish] if finish > start
|
|
566
640
|
end
|
|
567
|
-
@charges = AppManager::Charge.where(shop_domain: @shop_domain).order(created_at: :desc)
|
|
568
|
-
if @charges.any?
|
|
569
|
-
charge = @charges.first
|
|
570
641
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
642
|
+
if trial_activated_at.present?
|
|
643
|
+
choose_later_plan = AppManager::Plan.where(choose_later_plan: true).order(id: :desc).first
|
|
644
|
+
if choose_later_plan && choose_later_plan['trial_days'].to_i > 0
|
|
645
|
+
cl_extension = AppManager::ExtendTrial.where(shop_domain: shop_domain, plan_id: choose_later_plan['plan_id']).sum(:days).to_i
|
|
646
|
+
length = choose_later_plan['trial_days'].to_i + cl_extension
|
|
647
|
+
start = to_datetime_safe(trial_activated_at)
|
|
648
|
+
unless start.nil?
|
|
649
|
+
natural_end = start + length.days
|
|
650
|
+
finish = DateTime.now < natural_end ? DateTime.now : natural_end
|
|
651
|
+
# No-charge trial ends when the first charge starts.
|
|
652
|
+
finish = earliest_charge_start if earliest_charge_start && earliest_charge_start < finish
|
|
653
|
+
windows << [start, finish] if finish > start
|
|
574
654
|
end
|
|
655
|
+
end
|
|
656
|
+
end
|
|
575
657
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
658
|
+
return 0 if windows.empty?
|
|
659
|
+
|
|
660
|
+
windows.sort_by! { |w| w[0] }
|
|
661
|
+
|
|
662
|
+
total = 0
|
|
663
|
+
cur_start, cur_end = windows[0]
|
|
664
|
+
windows[1..-1].each do |start, finish|
|
|
665
|
+
if start <= cur_end
|
|
666
|
+
cur_end = finish if finish > cur_end
|
|
667
|
+
else
|
|
668
|
+
total += (cur_end - cur_start).to_i
|
|
669
|
+
cur_start, cur_end = start, finish
|
|
588
670
|
end
|
|
589
|
-
return @remaining_days
|
|
590
671
|
end
|
|
672
|
+
total += (cur_end - cur_start).to_i
|
|
673
|
+
|
|
674
|
+
total
|
|
591
675
|
end
|
|
592
676
|
|
|
593
677
|
|
|
@@ -601,6 +685,7 @@ module AppManager
|
|
|
601
685
|
charges = AppManager::Charge.where(status: 'active', shop_domain: params["shop_domain"])
|
|
602
686
|
if charges.any?
|
|
603
687
|
active_charge = charges.first.attributes
|
|
688
|
+
active_charge = active_charge.merge(effective_charge_pricing(active_charge))
|
|
604
689
|
end
|
|
605
690
|
cancelled_charges = AppManager::Charge.where(status: 'cancelled', shop_domain: params["shop_domain"]).order(created_at: :desc)
|
|
606
691
|
if cancelled_charges.any?
|
|
@@ -611,6 +696,78 @@ module AppManager
|
|
|
611
696
|
return charge_hash
|
|
612
697
|
end
|
|
613
698
|
|
|
699
|
+
# Offline mirror of the portal's effective-pricing calculation. Derives the true
|
|
700
|
+
# price the shop pays right now from the discount snapshot stored on the charge
|
|
701
|
+
# plus elapsed time, instead of recomputing from current discount config.
|
|
702
|
+
def effective_charge_pricing(charge)
|
|
703
|
+
base = (charge['price'] || 0).to_f.round(2)
|
|
704
|
+
no_discount = {
|
|
705
|
+
'effective_price' => base,
|
|
706
|
+
'strike_price' => nil,
|
|
707
|
+
'is_discount_active' => false,
|
|
708
|
+
'remaining_intervals' => nil,
|
|
709
|
+
'discount_ends_on' => nil
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
begin
|
|
713
|
+
now = DateTime.now
|
|
714
|
+
|
|
715
|
+
value = charge['discount_value']
|
|
716
|
+
return no_discount if value.nil? || value.to_f <= 0
|
|
717
|
+
|
|
718
|
+
type = charge['discount_type'] || 'percentage'
|
|
719
|
+
discounted = type == 'percentage' ? base - (base * value.to_f / 100) : base - value.to_f
|
|
720
|
+
discounted = [discounted, 0].max.round(2)
|
|
721
|
+
|
|
722
|
+
duration = (charge['discount_duration_intervals'] || 0).to_i
|
|
723
|
+
if duration <= 0
|
|
724
|
+
return {
|
|
725
|
+
'effective_price' => discounted,
|
|
726
|
+
'strike_price' => base,
|
|
727
|
+
'is_discount_active' => true,
|
|
728
|
+
'remaining_intervals' => nil,
|
|
729
|
+
'discount_ends_on' => nil
|
|
730
|
+
}
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
anchor = to_datetime_safe(charge['trial_ends_on'] || charge['activated_on'] || charge['created_at']) || now
|
|
734
|
+
|
|
735
|
+
interval_days = charge['interval'] == 'ANNUAL' ? 365 : 30
|
|
736
|
+
days_since_anchor = [(now - anchor).to_f, 0].max
|
|
737
|
+
elapsed = (days_since_anchor / interval_days).floor
|
|
738
|
+
ends_on = (anchor + (duration * interval_days)).to_date.to_s
|
|
739
|
+
|
|
740
|
+
if elapsed >= duration
|
|
741
|
+
return {
|
|
742
|
+
'effective_price' => base,
|
|
743
|
+
'strike_price' => nil,
|
|
744
|
+
'is_discount_active' => false,
|
|
745
|
+
'remaining_intervals' => 0,
|
|
746
|
+
'discount_ends_on' => ends_on
|
|
747
|
+
}
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
{
|
|
751
|
+
'effective_price' => discounted,
|
|
752
|
+
'strike_price' => base,
|
|
753
|
+
'is_discount_active' => true,
|
|
754
|
+
'remaining_intervals' => duration - elapsed,
|
|
755
|
+
'discount_ends_on' => ends_on
|
|
756
|
+
}
|
|
757
|
+
rescue Exception => e
|
|
758
|
+
Rollbar.error("APP MANAGER effective_charge_pricing failed #{e.inspect}") rescue nil
|
|
759
|
+
no_discount
|
|
760
|
+
end
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
def to_datetime_safe(value)
|
|
764
|
+
return nil if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
|
765
|
+
return value.to_datetime if value.respond_to?(:to_datetime)
|
|
766
|
+
DateTime.parse(value.to_s)
|
|
767
|
+
rescue Exception
|
|
768
|
+
nil
|
|
769
|
+
end
|
|
770
|
+
|
|
614
771
|
def get_local_has_plan(params, options)
|
|
615
772
|
if params["grandfathered"].present? && params["grandfathered"] == 1
|
|
616
773
|
return {"has_plan" => true}
|
|
@@ -707,7 +864,9 @@ module AppManager
|
|
|
707
864
|
test_value = charge["test"]
|
|
708
865
|
plan_id = charge["plan_id"].to_i
|
|
709
866
|
begin
|
|
710
|
-
|
|
867
|
+
attrs = { charge_id: charge["charge_id"], test: test_value, status: charge["status"], name: charge["name"], type: charge["type"], price: charge["price"], interval: charge["interval"], trial_days: charge["trial_days"], billing_on: charge["billing_on"], activated_on: charge["activated_on"], trial_ends_on: charge["trial_ends_on"], cancelled_on: charge["cancelled_on"], expires_on: charge["expires_on"], plan_id: plan_id, description: charge["description"], shop_domain: charge["shop_domain"], created_at: charge["created_at"], updated_at: charge["updated_at"], sync: 0, process_type: 'store-charge' }
|
|
868
|
+
add_charge_discount_snapshot(attrs, charge)
|
|
869
|
+
AppManager::Charge.create(attrs)
|
|
711
870
|
message = {"message" => 'success'}
|
|
712
871
|
rescue Exception => e
|
|
713
872
|
Rollbar.error("Charge not saved on local DB due to #{e.inspect}")
|
|
@@ -1030,6 +1189,18 @@ module AppManager
|
|
|
1030
1189
|
)
|
|
1031
1190
|
record.update!(filtered)
|
|
1032
1191
|
|
|
1192
|
+
when "app-faqs"
|
|
1193
|
+
begin
|
|
1194
|
+
AppManager::AppFaq.connection.truncate(AppManager::AppFaq.table_name)
|
|
1195
|
+
rescue
|
|
1196
|
+
AppManager::AppFaq.delete_all
|
|
1197
|
+
end
|
|
1198
|
+
Array(payload).each do |faq|
|
|
1199
|
+
faq = faq.is_a?(Hash) ? faq : {}
|
|
1200
|
+
filtered = serialize_row_for_failsafe(faq, date_fields, %w[pivot app_id])
|
|
1201
|
+
AppManager::AppFaq.create!(filtered) if filtered.present?
|
|
1202
|
+
end
|
|
1203
|
+
|
|
1033
1204
|
when "promotional-discounts-app-removed"
|
|
1034
1205
|
|
|
1035
1206
|
discount_id = payload["id"]
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
module AppManager
|
|
2
2
|
class GraphqlHelper
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
# Discount snapshot captured while building the last recurring charge, so the
|
|
5
|
+
# charge callback can persist it (value/type/duration/source) on the charge row.
|
|
6
|
+
attr_reader :discount_snapshot
|
|
7
|
+
|
|
4
8
|
def initialize(shopify_domain,shopify_token)
|
|
5
9
|
@api_key = AppManager.configuration.shopify_api_key || nil
|
|
6
10
|
@api_version = AppManager.configuration.shopify_api_version || nil
|
|
@@ -61,26 +65,13 @@ module AppManager
|
|
|
61
65
|
if !plan['affiliate'].nil? && plan['affiliate'].any? && !shop_plan_field.nil? && (plan['affiliate'].map{|e| e['value']}.include?(shop[shop_plan_field]) || shop[partner_development_field])
|
|
62
66
|
plan_test = true
|
|
63
67
|
end
|
|
64
|
-
trial_days = plan['trial_days'] || 0
|
|
65
|
-
if shop && shop.shopify_domain
|
|
68
|
+
trial_days = (plan['trial_days'] || 0).to_i
|
|
69
|
+
if shop && shop.shopify_domain
|
|
66
70
|
trial_activated_at = shop[AppManager.configuration.field_names['trial_activated_at']] rescue nil
|
|
67
|
-
plan_id_field = shop[AppManager.configuration.plan_id_or_name_field] rescue nil
|
|
68
71
|
remaining_obj = AppManager::Client.new
|
|
69
|
-
remaining = remaining_obj.get_remaining_days(shop.shopify_domain,trial_activated_at,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if !remaining.nil?
|
|
73
|
-
if !plan_id_field.nil?
|
|
74
|
-
plan_obj = AppManager::Client.new
|
|
75
|
-
current_plan = plan_obj.get_plan(plan_id_field) rescue nil
|
|
76
|
-
used_days = (current_plan['trial_days'].to_i - remaining.to_i) if current_plan rescue 0
|
|
77
|
-
if used_days > 0
|
|
78
|
-
days = trial_days - used_days
|
|
79
|
-
trial_days = days > 0 ? days : 0
|
|
80
|
-
end
|
|
81
|
-
else
|
|
82
|
-
trial_days = remaining
|
|
83
|
-
end
|
|
72
|
+
remaining = remaining_obj.get_remaining_days(shop.shopify_domain, trial_activated_at, plan['id'])
|
|
73
|
+
if remaining.is_a?(Numeric) || (remaining.is_a?(String) && remaining.match?(/\A-?\d+\z/))
|
|
74
|
+
trial_days = [0, remaining.to_i].max
|
|
84
75
|
end
|
|
85
76
|
end
|
|
86
77
|
|
|
@@ -91,54 +82,61 @@ module AppManager
|
|
|
91
82
|
promotional_discount = [] if promotional_discount.class.to_s == "Hash" && promotional_discount.has_key?('status') && promotional_discount['status'] == 404
|
|
92
83
|
end
|
|
93
84
|
|
|
94
|
-
|
|
85
|
+
# A promotional discount now takes priority over the plan's own discount when it
|
|
86
|
+
# applies to this (non-global) plan; otherwise fall back to the plan discount.
|
|
87
|
+
promo_applies = promotional_discount.present? && promotional_discount.respond_to?(:any?) && promotional_discount.any? &&
|
|
88
|
+
promotional_discount['value'].to_f > 0 &&
|
|
89
|
+
!(plan['is_global'] == 1 || plan['is_global'] == true) &&
|
|
90
|
+
(promotional_discount['plan_relation'].blank? || promotional_discount['plan_relation'].include?(plan['id']))
|
|
91
|
+
|
|
92
|
+
plan_discount = {}
|
|
93
|
+
@discount_snapshot = nil
|
|
94
|
+
|
|
95
|
+
if promo_applies
|
|
96
|
+
discount_type = promotional_discount['type'] || 'percentage'
|
|
97
|
+
discount_value = {
|
|
98
|
+
discount_type => discount_type == 'percentage' ? promotional_discount['value'].to_f / 100 : [promotional_discount['value'].to_f, plan['price']].min
|
|
99
|
+
}
|
|
100
|
+
discount = { 'value' => discount_value }
|
|
101
|
+
discount['durationLimitInIntervals'] = promotional_discount['duration_intervals'].to_i if promotional_discount['duration_intervals'].to_i.positive?
|
|
102
|
+
plan_discount = { "discount" => discount }
|
|
103
|
+
|
|
104
|
+
@discount_snapshot = {
|
|
105
|
+
'discount_value' => promotional_discount['value'].to_f,
|
|
106
|
+
'discount_type' => promotional_discount['type'] || 'percentage',
|
|
107
|
+
'discount_duration_intervals' => promotional_discount['duration_intervals'].to_i,
|
|
108
|
+
'discount_source' => 'promotional',
|
|
109
|
+
'promotional_discount_id' => (promotional_discount['id'] || 0).to_i
|
|
110
|
+
}
|
|
111
|
+
elsif !plan['discount'].nil? && plan['discount'] != 0
|
|
95
112
|
discount_type = plan['discount_type'] || "percentage"
|
|
96
113
|
discount_val = discount_type == "percentage" ? (plan['discount'].to_f/ 100) : "#{plan['discount']}"
|
|
97
114
|
if !@api_version.nil? && @api_version.to_s[/^\d{4}/].to_i >= 2024
|
|
98
|
-
plan_discount = if plan['
|
|
115
|
+
plan_discount = if plan['cycle_count'] && plan['cycle_count'].to_i != 0
|
|
99
116
|
{
|
|
100
117
|
"discount" => { "durationLimitInIntervals" => (plan['cycle_count'].to_i),
|
|
101
118
|
"value" => {"#{discount_type}" => discount_val}
|
|
102
119
|
}
|
|
103
120
|
}
|
|
104
|
-
|
|
121
|
+
else
|
|
105
122
|
{
|
|
106
123
|
"discount" => { "value" => {"#{discount_type}" => discount_val}
|
|
107
124
|
}
|
|
108
125
|
}
|
|
109
|
-
else
|
|
110
|
-
{}
|
|
111
126
|
end
|
|
112
127
|
else
|
|
113
|
-
plan_discount =
|
|
114
|
-
end
|
|
115
|
-
else
|
|
116
|
-
|
|
117
|
-
if promotional_discount.any?
|
|
118
|
-
|
|
119
|
-
if promotional_discount['plan_relation'].any? && !promotional_discount['plan_relation'].include?(plan['id']) && plan['is_global']
|
|
120
|
-
plan_discount = {}
|
|
121
|
-
else
|
|
122
|
-
|
|
123
|
-
discount_type = promotional_discount['type'] || 'percentage'
|
|
124
|
-
discount_value = {
|
|
125
|
-
discount_type => discount_type == 'percentage' ? promotional_discount['value'].to_f / 100 : [promotional_discount['value'].to_f, plan['price']].min
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
discount = { 'value' => discount_value }
|
|
129
|
-
|
|
130
|
-
discount['durationLimitInIntervals'] = promotional_discount['duration_intervals'].to_i if promotional_discount['duration_intervals'].to_i.positive?
|
|
131
|
-
plan_discount = { "discount" => discount}
|
|
132
|
-
|
|
133
|
-
end
|
|
134
|
-
|
|
128
|
+
plan_discount = { "discount" => { "durationLimitInIntervals" => (plan['cycle_count'].to_i), "value" => {"#{discount_type}" => discount_val} } }
|
|
135
129
|
end
|
|
136
130
|
|
|
131
|
+
@discount_snapshot = {
|
|
132
|
+
'discount_value' => plan['discount'].to_f,
|
|
133
|
+
'discount_type' => plan['discount_type'] || 'percentage',
|
|
134
|
+
'discount_duration_intervals' => plan['cycle_count'].to_i,
|
|
135
|
+
'discount_source' => plan['discount_is_custom'] ? 'custom' : 'plan'
|
|
136
|
+
}
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
-
|
|
140
|
-
promotional_discount_applies = promotional_discount.present? && promotional_discount.any?
|
|
141
|
-
promotional_discount_id = discount_exists && promotional_discount_applies ? 0 : promotional_discount_applies ? promotional_discount['id'] : 0
|
|
139
|
+
promotional_discount_id = promo_applies ? (promotional_discount['id'] || 0) : 0
|
|
142
140
|
|
|
143
141
|
return_url += "&discount=#{promotional_discount_id}"
|
|
144
142
|
|
data/lib/app_manager/version.rb
CHANGED
data/lib/app_manager.rb
CHANGED
|
@@ -26,6 +26,12 @@ module AppManager
|
|
|
26
26
|
@configuration ||= AppManager::Config.new
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
# Cache key for the discount snapshot captured while a charge is being created.
|
|
30
|
+
# Kept under a separate prefix from "app-manager-cache/*" so clear_cache does not wipe it.
|
|
31
|
+
def self.charge_snapshot_cache_key(shop, timestamp)
|
|
32
|
+
"app-manager:charge-snapshot:#{shop}:#{timestamp}"
|
|
33
|
+
end
|
|
34
|
+
|
|
29
35
|
def self.clear_cache
|
|
30
36
|
begin
|
|
31
37
|
Rails.cache.delete_matched("app-manager-cache/*")
|
|
@@ -66,6 +66,30 @@ module AppManager
|
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
+
def create_trial_days_text_most_popular_plan_badge_to_plans_migration
|
|
70
|
+
if self.class.migration_exists?("db/app_manager", "add_trial_days_text_most_popular_plan_badge_to_plans")
|
|
71
|
+
say_status("skipped", "Migration add_trial_days_text_most_popular_plan_badge_to_plans.rb already exists")
|
|
72
|
+
else
|
|
73
|
+
migration_template("add_trial_days_text_most_popular_plan_badge_to_plans.erb", "db/app_manager/add_trial_days_text_most_popular_plan_badge_to_plans.rb")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def create_app_faqs_table_migration
|
|
78
|
+
if self.class.migration_exists?("db/app_manager", "create_app_faqs_table")
|
|
79
|
+
say_status("skipped", "Migration create_app_faqs_table.rb already exists")
|
|
80
|
+
else
|
|
81
|
+
migration_template("create_app_faqs_table.erb", "db/app_manager/create_app_faqs_table.rb")
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def create_charge_discount_snapshot_and_used_migration
|
|
86
|
+
if self.class.migration_exists?("db/app_manager", "add_charge_discount_snapshot_and_used")
|
|
87
|
+
say_status("skipped", "Migration add_charge_discount_snapshot_and_used.rb already exists")
|
|
88
|
+
else
|
|
89
|
+
migration_template("add_charge_discount_snapshot_and_used.erb", "db/app_manager/add_charge_discount_snapshot_and_used.rb")
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
69
93
|
|
|
70
94
|
private
|
|
71
95
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class AddChargeDiscountSnapshotAndUsed < ActiveRecord::Migration[<%= rails_migration_version %>]
|
|
2
|
+
def change
|
|
3
|
+
add_column :charges, :discount_value, :decimal, precision: 8, scale: 2 unless column_exists?(:charges, :discount_value)
|
|
4
|
+
add_column :charges, :discount_type, :string unless column_exists?(:charges, :discount_type)
|
|
5
|
+
add_column :charges, :discount_duration_intervals, :integer unless column_exists?(:charges, :discount_duration_intervals)
|
|
6
|
+
add_column :charges, :discount_source, :string unless column_exists?(:charges, :discount_source)
|
|
7
|
+
add_column :charges, :promotional_discount_id, :bigint unless column_exists?(:charges, :promotional_discount_id)
|
|
8
|
+
|
|
9
|
+
add_column :discount_plans, :used, :boolean, default: false unless column_exists?(:discount_plans, :used)
|
|
10
|
+
add_column :plan_users, :used, :boolean, default: false unless column_exists?(:plan_users, :used)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
class AddTrialDaysTextMostPopularPlanBadgeToPlans < ActiveRecord::Migration[<%= rails_migration_version %>]
|
|
2
|
+
def change
|
|
3
|
+
add_column :plans, :trial_days_text, :text unless column_exists?(:plans, :trial_days_text)
|
|
4
|
+
add_column :plans, :most_popular, :boolean, default: false unless column_exists?(:plans, :most_popular)
|
|
5
|
+
add_column :plans, :plan_badge, :text unless column_exists?(:plans, :plan_badge)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class CreateAppFaqsTable < ActiveRecord::Migration[<%= rails_migration_version %>]
|
|
2
|
+
def change
|
|
3
|
+
create_table :app_faqs do |t|
|
|
4
|
+
t.text :question, null: false
|
|
5
|
+
t.text :answer, null: false
|
|
6
|
+
t.integer :sort_order, default: 0
|
|
7
|
+
t.timestamps
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def down
|
|
12
|
+
drop_table :app_faqs
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -29,6 +29,11 @@ class AddFailSafe < ActiveRecord::Migration[<%= rails_migration_version %>]
|
|
|
29
29
|
t.string :charge_id
|
|
30
30
|
t.boolean :test
|
|
31
31
|
t.float :price
|
|
32
|
+
t.decimal :discount_value, precision: 8, scale: 2
|
|
33
|
+
t.string :discount_type
|
|
34
|
+
t.integer :discount_duration_intervals
|
|
35
|
+
t.string :discount_source
|
|
36
|
+
t.bigint :promotional_discount_id
|
|
32
37
|
t.string :interval
|
|
33
38
|
t.integer :trial_days
|
|
34
39
|
t.datetime :billing_on
|
|
@@ -68,6 +73,7 @@ class AddFailSafe < ActiveRecord::Migration[<%= rails_migration_version %>]
|
|
|
68
73
|
t.integer :created_by
|
|
69
74
|
t.integer :app_id
|
|
70
75
|
t.string :discount_type
|
|
76
|
+
t.boolean :used, default: false
|
|
71
77
|
t.timestamps
|
|
72
78
|
end
|
|
73
79
|
create_table :extend_trials do |t|
|
|
@@ -84,6 +90,7 @@ class AddFailSafe < ActiveRecord::Migration[<%= rails_migration_version %>]
|
|
|
84
90
|
t.integer :plan_id
|
|
85
91
|
t.integer :app_id
|
|
86
92
|
t.integer :created_by
|
|
93
|
+
t.boolean :used, default: false
|
|
87
94
|
t.timestamps
|
|
88
95
|
end
|
|
89
96
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: app_manager
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rahul Tiwari @ Hulkapps
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: httparty
|
|
@@ -86,6 +86,7 @@ files:
|
|
|
86
86
|
- app/controllers/app_manager/plans_controller.rb
|
|
87
87
|
- app/controllers/concerns/app_manager/authenticate.rb
|
|
88
88
|
- app/model/app_manager/app.rb
|
|
89
|
+
- app/model/app_manager/app_faq.rb
|
|
89
90
|
- app/model/app_manager/app_manager_record.rb
|
|
90
91
|
- app/model/app_manager/app_structure.rb
|
|
91
92
|
- app/model/app_manager/charge.rb
|
|
@@ -124,12 +125,15 @@ files:
|
|
|
124
125
|
- lib/app_manager/tasks/sync/local_app_manager.rake
|
|
125
126
|
- lib/app_manager/version.rb
|
|
126
127
|
- lib/generators/app_manager/install/install_generator.rb
|
|
128
|
+
- lib/generators/app_manager/install/templates/add_charge_discount_snapshot_and_used.erb
|
|
127
129
|
- lib/generators/app_manager/install/templates/add_discount_tables.erb
|
|
128
130
|
- lib/generators/app_manager/install/templates/add_external_charge_field.erb
|
|
129
131
|
- lib/generators/app_manager/install/templates/add_is_global_to_plans.erb
|
|
130
132
|
- lib/generators/app_manager/install/templates/add_note_and_details_to_plans.erb
|
|
131
133
|
- lib/generators/app_manager/install/templates/add_plan_trial_grandfathered_to_shops.erb
|
|
134
|
+
- lib/generators/app_manager/install/templates/add_trial_days_text_most_popular_plan_badge_to_plans.erb
|
|
132
135
|
- lib/generators/app_manager/install/templates/app_manager.rb.tt
|
|
136
|
+
- lib/generators/app_manager/install/templates/create_app_faqs_table.erb
|
|
133
137
|
- lib/generators/app_manager/install/templates/failsafe_tables.erb
|
|
134
138
|
homepage: https://www.hulkapps.com
|
|
135
139
|
licenses: []
|