shopify-gold 2.0.0.pre

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.
Files changed (86) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +144 -0
  3. data/Rakefile +32 -0
  4. data/app/assets/config/gold_manifest.js +0 -0
  5. data/app/assets/images/gold/app-logo.svg +17 -0
  6. data/app/assets/images/gold/aside-bg.svg +54 -0
  7. data/app/assets/javascripts/gold/billing.js +30 -0
  8. data/app/assets/stylesheets/gold/billing.sass +64 -0
  9. data/app/assets/stylesheets/shopify/polaris.css +1 -0
  10. data/app/controllers/gold/admin/billing_controller.rb +190 -0
  11. data/app/controllers/gold/application_controller.rb +17 -0
  12. data/app/controllers/gold/authenticated_controller.rb +15 -0
  13. data/app/controllers/gold/billing_controller.rb +185 -0
  14. data/app/controllers/gold/concerns/merchant_facing.rb +85 -0
  15. data/app/jobs/app_uninstalled_job.rb +2 -0
  16. data/app/jobs/gold/after_authenticate_job.rb +36 -0
  17. data/app/jobs/gold/app_uninstalled_job.rb +10 -0
  18. data/app/jobs/gold/application_job.rb +20 -0
  19. data/app/mailers/gold/application_mailer.rb +7 -0
  20. data/app/mailers/gold/billing_mailer.rb +36 -0
  21. data/app/models/gold/billing.rb +157 -0
  22. data/app/models/gold/concerns/gilded.rb +22 -0
  23. data/app/models/gold/machine.rb +301 -0
  24. data/app/models/gold/shopify_plan.rb +70 -0
  25. data/app/models/gold/tier.rb +97 -0
  26. data/app/models/gold/transition.rb +26 -0
  27. data/app/operations/gold/accept_or_decline_charge_op.rb +119 -0
  28. data/app/operations/gold/accept_or_decline_terms_op.rb +26 -0
  29. data/app/operations/gold/apply_discount_op.rb +32 -0
  30. data/app/operations/gold/apply_tier_op.rb +51 -0
  31. data/app/operations/gold/charge_op.rb +99 -0
  32. data/app/operations/gold/check_charge_op.rb +50 -0
  33. data/app/operations/gold/cleanup_op.rb +20 -0
  34. data/app/operations/gold/convert_affiliate_to_paid_op.rb +32 -0
  35. data/app/operations/gold/freeze_op.rb +15 -0
  36. data/app/operations/gold/install_op.rb +30 -0
  37. data/app/operations/gold/issue_credit_op.rb +55 -0
  38. data/app/operations/gold/mark_as_delinquent_op.rb +21 -0
  39. data/app/operations/gold/resolve_outstanding_charge_op.rb +90 -0
  40. data/app/operations/gold/select_tier_op.rb +58 -0
  41. data/app/operations/gold/suspend_op.rb +21 -0
  42. data/app/operations/gold/uninstall_op.rb +20 -0
  43. data/app/operations/gold/unsuspend_op.rb +20 -0
  44. data/app/views/gold/admin/billing/_active_charge.erb +29 -0
  45. data/app/views/gold/admin/billing/_credit.erb +46 -0
  46. data/app/views/gold/admin/billing/_discount.erb +23 -0
  47. data/app/views/gold/admin/billing/_history.erb +65 -0
  48. data/app/views/gold/admin/billing/_overview.erb +14 -0
  49. data/app/views/gold/admin/billing/_shopify_plan.erb +21 -0
  50. data/app/views/gold/admin/billing/_state.erb +26 -0
  51. data/app/views/gold/admin/billing/_status.erb +25 -0
  52. data/app/views/gold/admin/billing/_tier.erb +155 -0
  53. data/app/views/gold/admin/billing/_trial_days.erb +42 -0
  54. data/app/views/gold/billing/_inner_head.html.erb +1 -0
  55. data/app/views/gold/billing/declined_charge.html.erb +20 -0
  56. data/app/views/gold/billing/expired_charge.html.erb +14 -0
  57. data/app/views/gold/billing/missing_charge.html.erb +22 -0
  58. data/app/views/gold/billing/outstanding_charge.html.erb +12 -0
  59. data/app/views/gold/billing/suspended.html.erb +8 -0
  60. data/app/views/gold/billing/terms.html.erb +22 -0
  61. data/app/views/gold/billing/tier.html.erb +29 -0
  62. data/app/views/gold/billing/transition_error.html.erb +9 -0
  63. data/app/views/gold/billing/unavailable.erb +8 -0
  64. data/app/views/gold/billing/uninstalled.html.erb +13 -0
  65. data/app/views/gold/billing_mailer/affiliate_to_paid.erb +23 -0
  66. data/app/views/gold/billing_mailer/delinquent.html.erb +21 -0
  67. data/app/views/gold/billing_mailer/suspension.html.erb +19 -0
  68. data/app/views/layouts/gold/billing.html.erb +22 -0
  69. data/app/views/layouts/gold/mailer.html.erb +13 -0
  70. data/app/views/layouts/gold/mailer.text.erb +1 -0
  71. data/config/routes.rb +33 -0
  72. data/db/migrate/01_create_gold_billing.rb +11 -0
  73. data/db/migrate/02_create_gold_transitions.rb +29 -0
  74. data/db/migrate/03_add_foreign_key_to_billing.rb +8 -0
  75. data/lib/gold/admin_engine.rb +8 -0
  76. data/lib/gold/billing_migrator.rb +107 -0
  77. data/lib/gold/coverage.rb +89 -0
  78. data/lib/gold/diagram.rb +40 -0
  79. data/lib/gold/engine.rb +16 -0
  80. data/lib/gold/exceptions/metadata_missing.rb +5 -0
  81. data/lib/gold/outcomes.rb +77 -0
  82. data/lib/gold/retries.rb +31 -0
  83. data/lib/gold/version.rb +3 -0
  84. data/lib/gold.rb +102 -0
  85. data/lib/tasks/gold_tasks.rake +94 -0
  86. metadata +298 -0
@@ -0,0 +1,190 @@
1
+ module Gold
2
+ module Admin
3
+ # Main controller for Gold to be used in app
4
+ class BillingController < ApplicationController
5
+ credentials = Gold.configuration.admin_credentials
6
+ http_basic_authenticate_with name: credentials[:user],
7
+ password: credentials[:password]
8
+
9
+ around_action :shopify_session
10
+
11
+ rescue_from Statesman::TransitionFailedError,
12
+ Statesman::GuardFailedError,
13
+ Gold::Exceptions::MetadataMissing do |e|
14
+ flash[:danger] = e.message
15
+ go_back
16
+ end
17
+
18
+ def transition
19
+ transitions = [params[:to]].flatten
20
+
21
+ transitions.each do |t|
22
+ billing.transition_to!(t, metadata)
23
+ end
24
+
25
+ flash[:success] = "Transitioned to '#{transitions.join(',')}'"
26
+ go_back
27
+ end
28
+
29
+ # rubocop:disable Metrics/LineLength:Length
30
+ def change_tier
31
+ tier = Tier.find(params[:tier])
32
+
33
+ outcome = SelectTierOp.new(billing, tier).call
34
+
35
+ if outcome.ok?
36
+ charge_outcome = ChargeOp.new(billing, process_charge_url).call
37
+
38
+ if charge_outcome.is_a?(Outcomes::ActiveCharge)
39
+ accept_or_decline_outcome = AcceptOrDeclineChargeOp.new(billing, charge_outcome.charge_id).call
40
+
41
+ if accept_or_decline_outcome.ok?
42
+ apply_tier_outcome = ApplyTierOp.new(billing).call
43
+
44
+ if apply_tier_outcome.ok? # rubocop:disable Metrics/BlockNesting
45
+ flash[:success] = "Changed billing to '#{tier.name}', no merchant action required"
46
+ else
47
+ flash[:danger] = "Failed to apply tier because '#{apply_tier_outcome.reason}'"
48
+ end
49
+ else
50
+ flash[:danger] = "Failed to accept or decline charge because '#{accept_or_decline_outcome.reason}'"
51
+ end
52
+ elsif charge_outcome.is_a?(Outcomes::ChargeNotNeeded)
53
+ flash[:success] = "Applied free tier '#{tier.name}', no merchant action required. You may need to cancel their current charge."
54
+ else
55
+ flash[:warning] = "Changing tier, but merchant action is required before tier is applied"
56
+ end
57
+ else
58
+ flash[:danger] = "Failed to apply tier because '#{outcome.reason}'"
59
+ end
60
+
61
+ go_back
62
+ end
63
+ # rubocop:enable Metrics/LineLength:Length
64
+
65
+ # Issue a credit to a shop
66
+ def issue_credit
67
+ outcome = IssueCreditOp.new(billing, params[:amount], params[:reason]).call
68
+
69
+ if outcome.ok?
70
+ flash["success"] = "Issued credit"
71
+ else
72
+ flash["danger"] = "Failed to issue credit because '#{outcome.message}'"
73
+ end
74
+
75
+ go_back
76
+ end
77
+
78
+ def cancel_charge
79
+ begin
80
+ charge = ShopifyAPI::RecurringApplicationCharge.find(params[:charge_id])
81
+ charge.cancel
82
+ flash["success"] = "Cancelled charge"
83
+ rescue ActiveResource::ResourceNotFound
84
+ flash["warning"] = "That charge was not found for '#{ShopifyAPI::Base.site}'"
85
+ end
86
+
87
+ go_back
88
+ end
89
+
90
+ # Suspend a shop
91
+ def suspend
92
+ if params[:confirm_suspend].blank?
93
+ flash[:danger] = "Please confirm you want to suspend this account"
94
+ go_back && return
95
+ end
96
+
97
+ SuspendOp.new(billing).call
98
+ flash[:success] = "Account suspended"
99
+ go_back
100
+ end
101
+
102
+ # Unsuspend a shop
103
+ def unsuspend
104
+ UnsuspendOp.new(billing).call
105
+ flash[:success] = "Account unsuspended"
106
+ go_back
107
+ end
108
+
109
+ def apply_discount
110
+ outcome = ApplyDiscountOp.new(billing,
111
+ params[:percentage].to_i,
112
+ process_charge_url).call
113
+
114
+ if outcome.ok?
115
+ flash[:warning] = "Discount applied, merchant must reauthorize the charge"
116
+ else
117
+ flash[:danger] = "Failed to apply discount because '#{outcome.message}'"
118
+ end
119
+
120
+ go_back
121
+ end
122
+
123
+ def override_shopify_plan
124
+ plan = params[:plan].presence
125
+ if billing.update(shopify_plan_override: plan)
126
+ flash[:success] = if plan
127
+ "Changed Shopify plan to '#{plan}'"
128
+ else
129
+ "Removed Shopify plan override"
130
+ end
131
+ else
132
+ flash[:danger] = "Failed to save shop"
133
+ end
134
+
135
+ go_back
136
+ end
137
+
138
+ def reset_trial_days
139
+ case params[:trial_action]
140
+ when "reset"
141
+ if billing.update(trial_starts_at: Time.current)
142
+ flash[:success] = "New trial starts... now! You might need to" \
143
+ "cancel their charge first."
144
+ else
145
+ flash[:danger] = "Failed to reset trial days"
146
+ end
147
+ when "clear"
148
+ if billing.update(trial_starts_at: nil)
149
+ flash[:success] = "Trial started over. The merchant will be faced" \
150
+ "with a new charge upon login."
151
+ else
152
+ flash[:danger] = "Failed to clear trial days"
153
+ end
154
+ else
155
+ flash[:danger] = "Don't know what to do with" \
156
+ " '#{params[:trial_action]}' trial action"
157
+ end
158
+
159
+ go_back
160
+ end
161
+
162
+ private
163
+
164
+ def shopify_session
165
+ billing.shop.with_shopify_session do
166
+ yield
167
+ end
168
+ end
169
+
170
+ def billing
171
+ @billing ||= Billing.find(params[:id])
172
+ end
173
+
174
+ def metadata
175
+ md = {
176
+ applied_by: "admin"
177
+ }
178
+
179
+ if params.dig(:metadata, :key).present?
180
+ md[params[:metadata][:key]] = params[:metadata][:value]
181
+ end
182
+ md
183
+ end
184
+
185
+ def go_back
186
+ redirect_back(fallback_location: main_app.gold_admin_engine_path)
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,17 @@
1
+ module Gold
2
+ # Controller that all Gold controllers inherit from.
3
+ class ApplicationController < ActionController::Base
4
+ protect_from_forgery with: :exception
5
+ around_action :catch_halt
6
+
7
+ layout "application"
8
+
9
+ private
10
+
11
+ # Allows an action to stop processing (like `return`, but works even in
12
+ # nested calls) and return a response to the user.
13
+ def catch_halt
14
+ catch(:halt) { yield }
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ require_dependency "gold/application_controller"
2
+
3
+ module Gold
4
+ # Similar to ShopifyApp::AuthenticatedController, but inherits from Gold's
5
+ # ApplicationController.
6
+ class AuthenticatedController < ApplicationController
7
+ # Include all of the contents of ShopifyApp::AuthenticatedController
8
+ include ShopifyApp::Localization
9
+ include ShopifyApp::LoginProtection
10
+ include ShopifyApp::EmbeddedApp
11
+
12
+ before_action :login_again_if_different_shop
13
+ around_action :shopify_session
14
+ end
15
+ end
@@ -0,0 +1,185 @@
1
+ module Gold
2
+ # Handles all merchant billing interactions.
3
+ class BillingController < AuthenticatedController
4
+ include Concerns::MerchantFacing
5
+ include Outcomes
6
+
7
+ layout "gold/billing"
8
+
9
+ rescue_from Statesman::TransitionFailedError do |e|
10
+ billing = Billing.find_by!(shop_id: session[:shopify])
11
+ Gold.logger.error("Shop '#{billing.shop.shopify_domain}' failed to " \
12
+ "transtion '#{e}'")
13
+ render "gold/billing/transition_error", layout: "gold/billing",
14
+ status: :internal_server_error
15
+ end
16
+
17
+ # Show the terms of service.
18
+ def terms
19
+ @already_accepted = !billing.can_transition_to?(:accepted_terms)
20
+ end
21
+
22
+ # Handle acceptance or declination of the terms of service.
23
+ def process_terms
24
+ accepted = params[:accept].present?
25
+ outcome = AcceptOrDeclineTermsOp.new(billing, accepted).call
26
+ case outcome
27
+ when AcceptedTerms
28
+ params.permit!
29
+ Gold.configuration.on_terms&.call(billing, params.to_h)
30
+ redirect_to select_tier_url
31
+ else
32
+ @error = true
33
+ render :terms
34
+ end
35
+ end
36
+
37
+ # Show the possible tiers to select from.
38
+ def tier
39
+ billing # Expose the @billing variable to the view
40
+ @tiers = Tier.visible
41
+ end
42
+
43
+ # Process a tier selection.
44
+ def select_tier
45
+ @tiers = Tier.visible
46
+ @tier = Tier.find(params[:tier])
47
+ outcome = SelectTierOp.new(billing, @tier).call
48
+ case outcome
49
+ when SameTier, TierApplied
50
+ redirect_to main_app.root_url
51
+ when CannotSelectTier
52
+ flash.now[:error] = "You may not select this tier currently"
53
+ render :tier
54
+ when ChargeNeeded
55
+ handle_charge_outcome ChargeOp.new(billing, process_charge_url).call
56
+ else
57
+ raise "Not sure how to handle #{outcome} on tier selection"
58
+ end
59
+ end
60
+
61
+ # Show the merchant that they have an outstanding charge and let them
62
+ # approve/decline it.
63
+ def outstanding_charge
64
+ outcome = ResolveOutstandingChargeOp.new(billing).call
65
+
66
+ case outcome
67
+ when PendingCharge
68
+ Gold.logger.info("[#{billing.id}] Charge is pending")
69
+ @confirmation_url = outcome.confirmation_url
70
+ when AcceptedCharge
71
+ Gold.logger.info("[#{billing.id}] Charge is accepted")
72
+ redirect_to outcome.return_url
73
+ when MissingCharge
74
+ Gold.logger.info("[#{billing.id}] Charge is missing")
75
+ redirect_to missing_charge_url
76
+ when ExpiredCharge
77
+ Gold.logger.info("[#{billing.id}] Charge is expired")
78
+ redirect_to expired_charge_url
79
+ when ActiveCharge
80
+ unless billing.current_state == :billing
81
+ AcceptOrDeclineChargeOp.new(billing, outcome.charge_id).call
82
+
83
+ unless ApplyTierOp.new(billing).call.ok?
84
+ raise "Charge was #{accepted_outcome}, but should have been active"
85
+ end
86
+
87
+ Gold.logger.info("[#{billing.id}] Charge is ready")
88
+ end
89
+
90
+ redirect_to main_app.root_url
91
+ else
92
+ raise "Not sure how to handle #{outcome} on outstanding charge"
93
+ end
94
+ end
95
+
96
+ # Process a charge confirmation when a merchant is redirected back from
97
+ # Shopify.
98
+ def process_charge
99
+ outcome = AcceptOrDeclineChargeOp.new(billing, params[:charge_id]).call
100
+
101
+ case outcome
102
+ when ActiveCharge
103
+ ApplyTierOp.new(billing).call
104
+ redirect_to main_app.root_url
105
+ when DeclinedCharge
106
+ redirect_to declined_charge_url
107
+ when ExpiredCharge
108
+ redirect_to expired_charge_url
109
+ when Uninstalled
110
+ redirect_to uninstalled_url
111
+ when CannotProcessCharge
112
+ redirect_to missing_charge_url
113
+ end
114
+ end
115
+
116
+ # Show the merchant that the charge has expired and direct them to try
117
+ # again.
118
+ def declined_charge
119
+ ensure_billing_is :sudden_charge_declined, :delayed_charge_declined
120
+ end
121
+
122
+ # Show the merchant that the charge has expired and direct them to try
123
+ # again.
124
+ def expired_charge
125
+ ensure_billing_is :sudden_charge_expired, :delayed_charge_expired
126
+ end
127
+
128
+ # Explain to the merchant that their charge is missing and direct them to
129
+ # try again.
130
+ def missing_charge
131
+ end
132
+
133
+ # Allow the merchant to retry the charge when necessary.
134
+ def retry_charge
135
+ handle_charge_outcome ChargeOp.new(billing, process_charge_url).call
136
+ end
137
+
138
+ # Show the merchant a suspension page if they have been suspended.
139
+ def suspended
140
+ ensure_billing_is :marked_as_suspended, :suspended
141
+ end
142
+
143
+ # Show the merchant a message about them uninstalling the app if they are
144
+ # still logged in.
145
+ def uninstalled
146
+ ensure_billing_is :marked_as_uninstalled,
147
+ :uninstalled,
148
+ :frozen,
149
+ :cleanup,
150
+ :done
151
+ end
152
+
153
+ private
154
+
155
+ # Redirect to the appropriate location based on the return value from
156
+ # ChargeOp.
157
+ def handle_charge_outcome(outcome)
158
+ Gold.logger.info("[#{billing.id}] Handling charge outcome")
159
+
160
+ case outcome
161
+ when ChargeNotNeeded
162
+ Gold.logger.info("[#{billing.id}] Charge is not needed")
163
+ redirect_to main_app.root_url
164
+ when ActiveCharge
165
+ Gold.logger.info("[#{billing.id}] Charge is active")
166
+ redirect_to main_app.root_url
167
+ when AcceptedCharge
168
+ Gold.logger.info("[#{billing.id}] Charge is accepted")
169
+ redirect_to outcome.return_url
170
+ when PendingCharge
171
+ Gold.logger.info("[#{billing.id}] Charge is pending")
172
+ redirect_to outcome.confirmation_url
173
+ end
174
+ end
175
+
176
+ # Ensure that the current merchant is in one of the provided states or
177
+ # redirect to the app's root URL.
178
+ def ensure_billing_is(*states)
179
+ return if states.include?(billing.current_state)
180
+
181
+ redirect_to main_app.root_url
182
+ throw :halt
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,85 @@
1
+ module Gold
2
+ module Concerns
3
+ # Helpers that make it easier to work with the current merchant's billing
4
+ # information in controllers.
5
+ module MerchantFacing
6
+ def self.included(base)
7
+ base.helper_method :billing
8
+ end
9
+
10
+ # Returns the Gold::Billing instance for the currently logged-in merchant.
11
+ def billing
12
+ @billing ||= Billing.find_by!(shop_id: session[:shopify])
13
+ end
14
+
15
+ # `before_action` filter that forces a merchant to deal with
16
+ # billing-related states (outstanding charges, acceptance of terms, etc)
17
+ # before continuing with their work.
18
+ def confront_mandatory_billing_action
19
+ ConfrontMandatoryBillingAction.new(self).call
20
+ end
21
+
22
+ # Provides access to routes, passing in a particular controller as
23
+ # context. We go through the effort of putting this into a separate class
24
+ # because we do not want to directly include the url helpers into whatever
25
+ # controller we are included into because that might override routes. This
26
+ # solution isolates the Gold routes from whatever routes the app that Gold
27
+ # is embedded in may have defined.
28
+ class HasAccessToRoutes
29
+ include Engine.routes.url_helpers
30
+ delegate :default_url_options, :url_options, to: :@context
31
+
32
+ def initialize(context)
33
+ @context = context
34
+ @engine = context.gold_engine
35
+ end
36
+ end
37
+
38
+ # See documentation on `confront_mandatory_billing_action`.
39
+ class ConfrontMandatoryBillingAction < HasAccessToRoutes
40
+ def call
41
+ begin
42
+ state = @context.billing.current_state
43
+ rescue ActiveRecord::RecordNotFound
44
+ state = nil
45
+ end
46
+
47
+ Gold.logger.info("Confronting billing, state is '#{state}'")
48
+
49
+ case state
50
+ when nil, :new
51
+ Gold.logger.info("Redirecting to terms page...")
52
+ return @context.redirect_to(@engine.terms_url)
53
+ when :select_tier, :reinstalled, :accepted_terms
54
+ Gold.logger.info("Redirecting to select tier...")
55
+ return @context.redirect_to(@engine.select_tier_url)
56
+ when :charge_missing
57
+ Gold.logger.info("Redirecting to missing charge...")
58
+ return @context.redirect_to(@engine.missing_charge_url)
59
+ when :sudden_charge, :delayed_charge, :optional_charge
60
+ Gold.logger.info("Redirecting to outstanding charge...")
61
+ return @context.redirect_to(@engine.outstanding_charge_url)
62
+ when :sudden_charge_declined, :delayed_charge_declined
63
+ Gold.logger.info("Redirecting to declined charge...")
64
+ return @context.redirect_to(@engine.declined_charge_url)
65
+ when :sudden_charge_expired, :delayed_charge_expired
66
+ Gold.logger.info("Redirecting to expired charge...")
67
+ return @context.redirect_to(@engine.expired_charge_url)
68
+ when :marked_as_delinquent, :delinquent
69
+ Gold.logger.info("Redirecting to missing charge...")
70
+ return @context.redirect_to(@engine.missing_charge_url)
71
+ when :marked_as_suspended, :suspended
72
+ Gold.logger.info("Redirecting to suspended page...")
73
+ return @context.redirect_to(@engine.suspended_path)
74
+ when :marked_as_uninstalled, :uninstalled, :frozen, :done
75
+ Gold.logger.info("Redirecting to uninstalled page...")
76
+ return @context.redirect_to(@engine.uninstalled_path)
77
+ when :cleanup
78
+ Gold.logger.info("Shop is cleanup, redirecting to unavailable...")
79
+ return @context.redirect_to(@engine.unavailable_path)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,2 @@
1
+ # Defers to the Gold implementation.
2
+ AppUninstalledJob = Gold::AppUninstalledJob
@@ -0,0 +1,36 @@
1
+ module Gold
2
+ # Run when the app is installed, reinstalled, or a user logs in. This is
3
+ # essentially how Gold is bootstrapped, so there is an assumption made
4
+ # here (the shop class has a `#with_shopify_session` method). This is true
5
+ # with the default Shop class that Shopify sets up, but if that is different
6
+ # for your environment, you will need to create your own custom version of
7
+ # this job.
8
+ class AfterAuthenticateJob < ApplicationJob
9
+ include Outcomes
10
+
11
+ def perform(shop_domain:)
12
+ # Look up the shop associated with the Shopify session.
13
+ shop = Gold.shop_class.find_by!(Gold.shop_domain_attribute => shop_domain)
14
+
15
+ # Create a billing instance if this store is newly installed or already
16
+ # cleaned up.
17
+ billing = Billing.find_or_create_by!(shop: shop)
18
+
19
+ shop.with_shopify_session do
20
+ outcome = InstallOp.new(billing).call
21
+
22
+ case outcome
23
+ when ChargeNeeded, MissingCharge
24
+ process_charge_url = Engine.routes.url_helpers.process_charge_url
25
+ logger.info("#{shop_domain} has reinstalled and needs to approve a " \
26
+ "charge for their selected tier")
27
+ ChargeOp.new(billing, process_charge_url).call
28
+ when Success
29
+ logger.info("#{shop_domain} successfully installed the app")
30
+ end
31
+
32
+ outcome
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,10 @@
1
+ module Gold
2
+ # Uninstalls the app for the merchant.
3
+ class AppUninstalledJob < ApplicationJob
4
+ def perform(shop_domain:, webhook: nil) # rubocop:disable Lint/UnusedMethodArgument
5
+ shop = Gold.shop_class.find_by!(Gold.shop_domain_attribute => shop_domain)
6
+ billing = Gold::Billing.find_by!(shop: shop)
7
+ UninstallOp.new(billing).call
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ module Gold
2
+ # Base for all Gold-related background jobs to build upon.
3
+ class ApplicationJob < ActiveJob::Base
4
+ include Engine.routes.url_helpers
5
+
6
+ queue_as :gold
7
+
8
+ # Automatically retry jobs that encountered a deadlock
9
+ # retry_on ActiveRecord::Deadlocked
10
+
11
+ # Most jobs are safe to ignore if the underlying records are no longer available
12
+ # discard_on ActiveJob::DeserializationError
13
+
14
+ protected
15
+
16
+ def default_url_options
17
+ Rails.application.config.default_url_options
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,7 @@
1
+ module Gold
2
+ # Defaults for all Gold-related mailers.
3
+ class ApplicationMailer < ActionMailer::Base
4
+ default from: Gold.configuration.contact_email
5
+ layout "mailer"
6
+ end
7
+ end
@@ -0,0 +1,36 @@
1
+ module Gold
2
+ # Sends billing-related emails to merchants.
3
+ class BillingMailer < ApplicationMailer
4
+ layout "gold/mailer"
5
+
6
+ # Inform a merchant about their account becoming suspended.
7
+ def suspension(billing)
8
+ billing.shop.with_shopify_session do
9
+ @shop = ShopifyAPI::Shop.current
10
+ mail(to: @shop.email,
11
+ subject: "We have suspended your access to #{Gold.configuration.app_name}")
12
+ end
13
+ end
14
+
15
+ # Inform a merchant about their non-payment.
16
+ def delinquent(billing)
17
+ billing.shop.with_shopify_session do
18
+ @shop = ShopifyAPI::Shop.current
19
+ mail(to: @shop.email,
20
+ subject: "We need you to approve a charge for " \
21
+ "#{Gold.configuration.app_name}")
22
+ end
23
+ end
24
+
25
+ def affiliate_to_paid(billing, confirmation_url)
26
+ billing.shop.with_shopify_session do
27
+ @shop = ShopifyAPI::Shop.current
28
+ @confirmation_url = confirmation_url
29
+ app_name = Gold.configuration.app_name
30
+
31
+ mail(to: @shop.email,
32
+ subject: "[Action Required] Charge needed for #{app_name}")
33
+ end
34
+ end
35
+ end
36
+ end