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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d059334cfd6240a77f00920ff84d496f0360f99a3af5a363e822969fd682e1c7
4
- data.tar.gz: 3f613e46ff5db1cb6afc5f8e3954f6a371c5426bba15c1ca28161811c010f8f7
3
+ metadata.gz: b07aa041091f2f5c6acc9fc85b9ffbf6c73d7d97f0694981fb3ee261fd4d167c
4
+ data.tar.gz: 3ef460faa396f6dbb9df6cb66010045975cd76b28a29e97d00b4542bb846a0c2
5
5
  SHA512:
6
- metadata.gz: 24708a6b21a077f54c68192b963edbc1057e93fefd45a60dc21bf54de20e1ac5e2402d99282e0df377c133045dd0971e7f6a1caea55bd6f79b5afd168a79faf5
7
- data.tar.gz: 948de12ae256d9bf53bb06951f3c362b924a01ab20609b0df4da214016e4aac5b4d620d842de039951e68185896ace49a43307acbd34eb9ca930b6123da52139
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.11)
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.18)
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.6)
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.6)
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.4)
106
+ erb (6.0.6)
107
107
  erubi (1.13.1)
108
- globalid (1.3.0)
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.14.8)
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.19.5)
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.1)
137
+ loofah (2.25.2)
138
138
  crass (~> 1.0.2)
139
139
  nokogiri (>= 1.12.0)
140
- mail (2.9.0)
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.0)
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.3-x86_64-linux-gnu)
165
+ nokogiri (1.19.4)
166
+ mini_portile2 (~> 2.8.2)
165
167
  racc (~> 1.4)
166
- pp (0.6.3)
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.0)
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
- rdoc (7.2.0)
219
+ rbs (4.0.3)
220
+ logger
221
+ prism (>= 1.6.0)
222
+ tsort
223
+ rdoc (8.0.0)
219
224
  erb
220
- psych (>= 4.0.0)
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.0)
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.7.5)
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], @trial_activated_field => nil,grandfathered_field => 0}
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
- request_data = {'shop' => @shop.shopify_domain, 'timestamp' => Time.now.to_i, 'plan' => params[:plan_id]}
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], trial_activated_field => nil,grandfathered_field => 0}
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, trial_activated_field => nil,grandfathered_field => 0}
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?) or !trial_activated_at) ? true : false),
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
- update_info = {@plan_field => params[:plan_id], @trial_activated_field => DateTime.now}
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
@@ -0,0 +1,5 @@
1
+ module AppManager
2
+ class AppFaq < AppManagerRecord
3
+ self.table_name = "app_faqs"
4
+ end
5
+ end
@@ -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'
@@ -76,6 +76,10 @@ module AppManager
76
76
  get("/get-related-discounted-plans?discount_id=#{discount_id}")
77
77
  end
78
78
 
79
+ def get_app_faqs
80
+ get("/app-faqs")
81
+ end
82
+
79
83
  end
80
84
  end
81
85
  end
@@ -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
- charge_data << AppManager::Charge.new(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)
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
- discount_plans_data << AppManager::DiscountPlan.new(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'])
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
- extend_plan_users << AppManager::PlanUser.new(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'])
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
- @remaining_days = 0
544
- @shop_domain = params['shop_domain']
545
- if params && params['trial_activated_at'].present? && !params['trial_activated_at'].nil? && params['shop_domain'].present? && params['plan_id'].present? && !params['plan_id'].nil?
546
- @trial_activated_at = params['trial_activated_at']
547
- @plan_id = params['plan_id']
548
- plan_data = AppManager::Plan.where(plan_id: @plan_id)
549
- if plan_data.any?
550
- trial_days = plan_data.first['trial_days']
551
- trial_start_date = Date.parse(@trial_activated_at)
552
- trial_end_date = trial_start_date + trial_days.days
553
- if trial_end_date > DateTime.now
554
- @remaining_days = (trial_end_date - DateTime.now).to_i
555
- end
556
- # return @remaining_days.inspect
557
- trial_extension_data = AppManager::ExtendTrial.where(shop_domain: @shop_domain, plan_id: @plan_id).order(extend_trial_start_at: :desc)
558
- if trial_extension_data.any?
559
- trial_extension_data = trial_extension_data.first
560
- extend_trial_date = trial_extension_data['created_at'].to_datetime + trial_extension_data['days'].to_i.days
561
- remaining_extended_days = DateTime.now < extend_trial_date ? (extend_trial_date - DateTime.now).to_i : 0
562
- @remaining_days = @remaining_days + remaining_extended_days
563
- end
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
- return @remaining_days
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
- if charge['trial_days']
572
- if charge['trial_ends_on'] && DateTime.now < charge['trial_ends_on']
573
- @remaining_days = (charge['trial_ends_on'].to_datetime - DateTime.now.to_datetime).to_i
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
- # ADD EXTRA DAY
577
- if charge['created_at'] && ((charge['created_at'].to_datetime - DateTime.now.to_datetime).to_i == 0)
578
- @remaining_days = @remaining_days + 1
579
- end
580
- # TODO: Uncomment this code when we implement Shopify trial extension apis
581
- # trial_extension_data = AppManager::ExtendTrial.where(shop_domain: @shop_domain, plan_id: @plan_id).order(extend_trial_start_at: :desc)
582
- # if trial_extension_data.any?
583
- # trial_extension_data = trial_extension_data.first
584
- # extend_trial_date = trial_extension_data['created_at'] + trial_extension_data['days'].to_i.days
585
- # remaining_extended_days = DateTime.now < extend_trial_date ? (extend_trial_date - DateTime.now).to_i : 0
586
- # @remaining_days = @remaining_days + remaining_extended_days
587
- # end
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
- AppManager::Charge.create(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')
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 && trial_days
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,plan_id_field)
70
- # trial_days = !remaining.nil? ? remaining : trial_days
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
- if !plan['discount'].nil? && plan['discount'] != 0
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['discount'] && plan['cycle_count'] && plan['cycle_count'].to_i != 0
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
- elsif plan['discount']
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 = plan['discount'] ? { "discount" => { "durationLimitInIntervals" => (plan['cycle_count'].to_i), "value" => {"#{discount_type}" => discount_val} } } : {}
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
- discount_exists = !plan['discount'].nil? && plan['discount'] != 0
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AppManager
4
- VERSION = "2.4.11"
4
+ VERSION = "2.7.0"
5
5
  end
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.11
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-05-14 00:00:00.000000000 Z
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: []