disco_app 0.12.7.pre.puma.pre.3 → 0.13.6.pre.puma.pre.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/components/disco_app/forms/model-form.es6.jsx +2 -7
  3. data/app/assets/javascripts/disco_app/components/custom/shop_row.js.jsx +2 -1
  4. data/app/assets/javascripts/disco_app/components/ui-kit/forms/base_form.es6.jsx +16 -44
  5. data/app/assets/javascripts/disco_app/components/ui-kit/forms/input-checkbox.es6.jsx +10 -5
  6. data/app/controllers/disco_app/admin/application_controller.rb +2 -1
  7. data/app/controllers/disco_app/admin/concerns/sources_controller.rb +51 -0
  8. data/app/controllers/disco_app/admin/sources_controller.rb +3 -0
  9. data/app/controllers/disco_app/concerns/user_authenticated_controller.rb +17 -0
  10. data/app/controllers/disco_app/user_sessions_controller.rb +57 -0
  11. data/app/jobs/disco_app/concerns/app_installed_job.rb +1 -1
  12. data/app/jobs/disco_app/concerns/app_uninstalled_job.rb +1 -1
  13. data/app/jobs/disco_app/concerns/render_asset_group_job.rb +1 -1
  14. data/app/jobs/disco_app/concerns/shop_update_job.rb +1 -1
  15. data/app/jobs/disco_app/concerns/subscription_changed_job.rb +1 -1
  16. data/app/jobs/disco_app/concerns/synchronise_carrier_service_job.rb +1 -1
  17. data/app/jobs/disco_app/concerns/synchronise_resources_job.rb +1 -1
  18. data/app/jobs/disco_app/concerns/synchronise_users_job.rb +15 -0
  19. data/app/jobs/disco_app/concerns/synchronise_webhooks_job.rb +1 -1
  20. data/app/jobs/disco_app/send_subscription_job.rb +1 -1
  21. data/app/jobs/disco_app/shop_job.rb +10 -3
  22. data/app/jobs/disco_app/synchronise_users_job.rb +3 -0
  23. data/app/models/disco_app/concerns/shop.rb +13 -4
  24. data/app/models/disco_app/concerns/source.rb +14 -0
  25. data/app/models/disco_app/concerns/subscription.rb +1 -1
  26. data/app/models/disco_app/concerns/synchronises.rb +9 -0
  27. data/app/models/disco_app/concerns/taggable.rb +7 -3
  28. data/app/models/disco_app/concerns/user.rb +20 -0
  29. data/app/models/disco_app/source.rb +3 -0
  30. data/app/models/disco_app/user.rb +3 -0
  31. data/app/resources/disco_app/admin/resources/concerns/shop_resource.rb +4 -4
  32. data/app/services/disco_app/subscription_service.rb +8 -2
  33. data/app/views/disco_app/admin/sources/_form.html.erb +34 -0
  34. data/app/views/disco_app/admin/sources/edit.html.erb +7 -0
  35. data/app/views/disco_app/admin/sources/index.html.erb +32 -0
  36. data/app/views/disco_app/admin/sources/new.html.erb +7 -0
  37. data/app/views/disco_app/user_sessions/new.html.erb +12 -0
  38. data/app/views/layouts/admin/_nav_items.erb +7 -0
  39. data/config/routes.rb +4 -0
  40. data/db/migrate/20170315062548_create_disco_app_sources.rb +10 -0
  41. data/db/migrate/20170315062629_add_sources_to_shop_subscriptions.rb +14 -0
  42. data/db/migrate/20170327214540_create_disco_app_users.rb +13 -0
  43. data/db/migrate/20170606160751_fix_disco_app_users_index.rb +6 -0
  44. data/lib/disco_app/version.rb +1 -1
  45. data/lib/generators/disco_app/disco_app_generator.rb +1 -1
  46. data/lib/generators/disco_app/templates/config/database.yml.tt +1 -0
  47. data/lib/tasks/users.rake +10 -0
  48. data/test/dummy/app/jobs/carts_update_job.rb +1 -1
  49. data/test/dummy/app/jobs/disco_app/app_uninstalled_job.rb +2 -2
  50. data/test/dummy/app/jobs/products_create_job.rb +1 -1
  51. data/test/dummy/app/jobs/products_delete_job.rb +1 -1
  52. data/test/dummy/app/jobs/products_update_job.rb +1 -1
  53. data/test/dummy/app/models/cart.rb +3 -3
  54. data/test/dummy/app/models/disco_app/shop.rb +1 -1
  55. data/test/dummy/config/database.yml +1 -0
  56. data/test/dummy/db/schema.rb +23 -2
  57. data/test/fixtures/api/subscriptions/valid_request.json +1 -1
  58. data/test/fixtures/api/widget_store/users.json +42 -0
  59. data/test/fixtures/disco_app/sources.yml +3 -0
  60. data/test/integration/synchronises_test.rb +3 -3
  61. data/test/jobs/disco_app/app_installed_job_test.rb +3 -3
  62. data/test/jobs/disco_app/app_uninstalled_job_test.rb +1 -1
  63. data/test/jobs/disco_app/synchronise_users_job_test.rb +26 -0
  64. metadata +27 -3
  65. data/test/dummy/config/database.gitlab-ci.yml +0 -24
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 00ea85f26e287b6a9f785c3e9761064244e39bc6
4
- data.tar.gz: 5564413348495e202135e4b6209c1204da54148e
3
+ metadata.gz: bf27c28d441f98e6eefdc7809f69daca79c75604
4
+ data.tar.gz: 9686a13bb881a26dd0185eb5f7673c211c8eebb5
5
5
  SHA512:
6
- metadata.gz: 56b1b6b57952ac3df28088a1866e1d79c67932bc77ea0120ba1b094645033ec877068f16b12f21fbaa54d1c335b0fa1b0a20c27c5bd59f96cc77f382be5ae006
7
- data.tar.gz: b04189e5088811e22ae96d6d1bacec8c3a6b35f200c9ef3223da11c836520474dc7c548457e0e9fa9dc6fde15a83cb84edfda6db402fab2bfd32bfebaf2cc6e7
6
+ metadata.gz: f4a2afea46cb6cbbf4e33baf5cbeb2b533ba6f9b9032b40f6a76d984a18977179fedf1fc656c1f750fc383e54906cb824f633447b7481f42267950c7ad921d16
7
+ data.tar.gz: 8f90a604e1a61b4b9b5f617dbd7d77a0406ccca51d82d6ab46638b2ab1293b3b1483031a2e33051cf0770a46dbf8b44bc33ab7aaf79fd947bf9c034064ca474b
@@ -2,7 +2,7 @@ class ModelForm extends BaseForm {
2
2
 
3
3
  render() {
4
4
  const { modelTitle, modelName, modelUrl, modelsUrl, children, authenticityToken } = this.props;
5
- const errors = this.renderErrors();
5
+ const errorsElement = this.getErrorsElement();
6
6
 
7
7
  return(
8
8
  <form action={modelUrl ? modelUrl : modelsUrl} acceptCharset="UTF-8" method="POST" data-shopify-app-submit="ea.save">
@@ -10,12 +10,7 @@ class ModelForm extends BaseForm {
10
10
  <input type="hidden" name="_method" value={modelUrl ? 'patch' : 'post'} />
11
11
  <input type="hidden" name="authenticity_token" value={authenticityToken}/>
12
12
 
13
- {(() => {
14
- if (!errors) return false;
15
- return (
16
- {errors}
17
- );
18
- })()}
13
+ {errorsElement}
19
14
 
20
15
  {children}
21
16
 
@@ -29,7 +29,8 @@ var ShopRow = (props) => {
29
29
  return (
30
30
  <tr>
31
31
  <td>
32
- <a href={editShopUrl}>{domainName}</a><br />
32
+ {domainName}
33
+ <br />
33
34
  <a href={'mailto:' + shop.attributes.email}>{shop.attributes.email}</a>
34
35
  </td>
35
36
  <td>{shop.attributes.status}</td>
@@ -5,64 +5,36 @@
5
5
  class BaseForm extends React.Component {
6
6
 
7
7
  /**
8
- * check if the field parameter has errors associated.
9
- * if no parameters are given, it checks if there's at least an error at all
10
- **/
11
- hasErrors(field) {
12
- if ((arguments.length === 0 && this.errorKeys().length > 0) ||
13
- (this.props.errors && this.props.errors.errors.indexOf(field) > -1)) {
14
- return true;
15
- }
16
- return false;
17
- }
18
-
19
- /**
20
- * returns a list of fields that contain errors
21
- **/
22
- errorKeys() {
23
- return this.props.errors.errors;
24
- }
25
-
26
- /**
27
- * returns the type of resource associate with this error
28
- **/
29
- errorType() {
30
- return this.props.errors.type;
31
- }
32
-
33
- /**
34
- * returns the error messages
35
- **/
36
- errorMessages() {
37
- return this.props.errors.messages;
38
- }
39
-
40
- /**
41
- * renders basic form errors
42
- **/
43
- renderErrors() {
44
- if (!this.hasErrors()) {
8
+ * Returns the JSX required to render a list of errors.
9
+ *
10
+ * @returns {*}
11
+ */
12
+ getErrorsElement() {
13
+ const { errors } = this.props;
14
+
15
+ // Don't render anything if no errors present.
16
+ if(!errors || !errors.errors || (errors.errors.length == 0)) {
45
17
  return null;
46
18
  }
47
19
 
48
- var title = "There " + (this.errorMessages().length == 1 ? "is" : "are") + " " + this.errorMessages().length + " error" + (this.errorMessages().length > 1 ? "s" : "") + " for this " + this.errorType() + ":";
49
- var errorMessages = this.errorMessages().map(function(message) {
50
- return <li>{message}</li>;
51
- });
20
+ const errorCount = errors.errors.length;
21
+ const singleError = (errorCount == 1);
52
22
 
53
23
  return (
54
24
  <div className="ui-banner ui-banner--status-error ui-banner--default-vertical-spacing ui-banner--default-horizontal-spacing">
55
25
  <div className="ui-banner__ribbon">
56
26
  <svg className="next-icon next-icon--24" viewBox="0 0 24 24">
57
- <path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.6 0 12 0zm0 4c1.4 0 2.7.4 3.9 1L12 8.8 8.8 12 5 15.9c-.6-1.1-1-2.5-1-3.9 0-4.4 3.6-8 8-8zm0 16c-1.4 0-2.7-.4-3.9-1l3.9-3.9 3.2-3.2L19 8.1c.6 1.1 1 2.5 1 3.9 0 4.4-3.6 8-8 8z"></path>
27
+ <use xmlns="http://www.w3.org/1999/xlink" xlinkHref="#next-error" />
58
28
  </svg>
59
29
  </div>
60
30
  <div className="ui-banner__content">
61
31
  <h2 className="ui-banner__title">
62
- {title}
32
+ There {singleError ? 'is' : 'are'} {errorCount} error{singleError ? '' : 's'} for this {errors.type}:
63
33
  </h2>
64
34
  <ul>
65
- {errorMessages}
35
+ {errors.messages.map((message, i) => {
36
+ return <li key={i}>{message}</li>;
37
+ })}
66
38
  </ul>
67
39
  </div>
68
40
  </div>
@@ -1,6 +1,6 @@
1
- const InputCheckbox = ({ label, name, value, checked, inline, isLast, onChange, disabled = false }) => {
1
+ const InputCheckbox = ({ label, name, checked, inline, isLast, onChange, disabled = false }) => {
2
2
 
3
- const id = `${name}[${value}]`;
3
+ const id = name;
4
4
 
5
5
  const wrapperClassName = classNames({
6
6
  'next-input-wrapper': true,
@@ -16,14 +16,19 @@ const InputCheckbox = ({ label, name, value, checked, inline, isLast, onChange,
16
16
  });
17
17
 
18
18
  const handleChange = (e) => {
19
- onChange && onChange(e.target.value);
19
+ onChange && onChange(e.target.checked);
20
20
  };
21
21
 
22
22
  return(
23
23
  <div className={wrapperClassName}>
24
24
  <label htmlFor={id} className={labelClassName}>{label}</label>
25
- <input id={id} className="next-checkbox" type="checkbox" value={value} name={name} checked={checked} onChange={handleChange} disabled={disabled} />
26
- <span className="next-checkbox--styled" />
25
+ <input type="hidden" value="0" name={name} />
26
+ <input id={id} className="next-checkbox" type="checkbox" value="1" name={name} checked={checked} onChange={handleChange} disabled={disabled} />
27
+ <span className="next-checkbox--styled">
28
+ <svg className="next-icon next-icon--size-10 next-icon--blue checkmark">
29
+ <use xmlns="http://www.w3.org/1999/xlink" xlinkHref="#next-checkmark" />
30
+ </svg>
31
+ </span>
27
32
  </div>
28
33
  )
29
34
 
@@ -5,6 +5,7 @@ class DiscoApp::Admin::ApplicationController < ActionController::Base
5
5
 
6
6
  helper_method :current_shop
7
7
  def current_shop
8
- @current_shop ||= @shop
8
+ @shop
9
9
  end
10
+
10
11
  end
@@ -0,0 +1,51 @@
1
+ module DiscoApp::Admin::Concerns::SourcesController
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ before_action :find_source, only: [:edit, :update, :destroy]
6
+ end
7
+
8
+ def index
9
+ @sources = DiscoApp::Source.all
10
+ end
11
+
12
+ def new
13
+ @source = DiscoApp::Source.new
14
+ end
15
+
16
+ def create
17
+ @source = DiscoApp::Source.new(source_params)
18
+ if @source.save
19
+ redirect_to admin_sources_path
20
+ else
21
+ render 'new'
22
+ end
23
+ end
24
+
25
+ def edit
26
+ end
27
+
28
+ def update
29
+ if @source.update_attributes(source_params)
30
+ redirect_to edit_admin_plan_path(@source)
31
+ else
32
+ render 'edit'
33
+ end
34
+ end
35
+
36
+ def destroy
37
+ source.destroy
38
+ redirect_to admin_sources_path
39
+ end
40
+
41
+ private
42
+
43
+ def find_source
44
+ @source = DiscoApp::Source.find(params[:id])
45
+ end
46
+
47
+ def source_params
48
+ params.require(:source).permit(:source, :name)
49
+ end
50
+
51
+ end
@@ -0,0 +1,3 @@
1
+ class DiscoApp::Admin::SourcesController < DiscoApp::Admin::ApplicationController
2
+ include DiscoApp::Admin::Concerns::SourcesController
3
+ end
@@ -0,0 +1,17 @@
1
+ module DiscoApp::Concerns::UserAuthenticatedController
2
+ extend ActiveSupport::Concern
3
+ include ShopifyApp::LoginProtection
4
+
5
+ included do
6
+ before_action :shopify_user
7
+ end
8
+
9
+ private
10
+
11
+ def shopify_user
12
+ @user = DiscoApp::User.find(session[:shopify_user])
13
+ rescue ActiveRecord::RecordNotFound
14
+ redirect_to disco_app.new_user_session_path
15
+ end
16
+
17
+ end
@@ -0,0 +1,57 @@
1
+ class DiscoApp::UserSessionsController < ApplicationController
2
+ include DiscoApp::Concerns::AuthenticatedController
3
+
4
+ def new
5
+ authenticate if sanitized_shop_name.present?
6
+ end
7
+
8
+ def create
9
+ authenticate
10
+ end
11
+
12
+ def callback
13
+ if auth_hash
14
+ login_user
15
+ redirect_to return_address
16
+ else
17
+ redirect_to root_path
18
+ end
19
+ end
20
+
21
+ def destroy
22
+ session[:shopify_user] = nil
23
+ redirect_to root_path
24
+ end
25
+
26
+ protected
27
+
28
+ def auth_hash
29
+ request.env['omniauth.auth']
30
+ end
31
+
32
+ def associated_user(auth_hash)
33
+ auth_hash['extra']['associated_user']
34
+ end
35
+
36
+ def authenticate
37
+ if sanitized_shop_name.present?
38
+ fullpage_redirect_to "#{main_app.root_path}auth/shopify_user?shop=#{sanitized_shop_name}"
39
+ else
40
+ redirect_to return_address
41
+ end
42
+ end
43
+
44
+ def login_user
45
+ @user = DiscoApp::User.create_user(associated_user(auth_hash), @shop)
46
+ session[:shopify_user] = @user.id
47
+ end
48
+
49
+ def return_address
50
+ session.delete(:return_to) || main_app.root_url
51
+ end
52
+
53
+ def sanitized_shop_name
54
+ @shop.shopify_domain
55
+ end
56
+
57
+ end
@@ -14,7 +14,7 @@ module DiscoApp::Concerns::AppInstalledJob
14
14
  # - Perform initial update of shop information.
15
15
  # - Subscribe to default plan, if any exists.
16
16
  #
17
- def perform(shop, plan_code = nil, source = nil)
17
+ def perform(_shop, plan_code = nil, source = nil)
18
18
  DiscoApp::SynchroniseWebhooksJob.perform_now(@shop)
19
19
  DiscoApp::SynchroniseCarrierServiceJob.perform_now(@shop)
20
20
  DiscoApp::ShopUpdateJob.perform_now(@shop)
@@ -12,7 +12,7 @@ module DiscoApp::Concerns::AppUninstalledJob
12
12
  # - Mark any recurring application charges as cancelled.
13
13
  # - Remove any stored sessions for the shop.
14
14
  #
15
- def perform(shop, shop_data)
15
+ def perform(_shop, shop_data)
16
16
  DiscoApp::ChargesService.cancel_recurring_charges(@shop)
17
17
  DiscoApp::SendSubscriptionJob.perform_later(@shop)
18
18
  @shop.sessions.delete_all
@@ -1,7 +1,7 @@
1
1
  module DiscoApp::Concerns::RenderAssetGroupJob
2
2
  extend ActiveSupport::Concern
3
3
 
4
- def perform(shop, instance, asset_group)
4
+ def perform(_shop, instance, asset_group)
5
5
  instance.render_asset_group(asset_group.to_sym)
6
6
  end
7
7
 
@@ -2,7 +2,7 @@ module DiscoApp::Concerns::ShopUpdateJob
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  # Perform an update of the current shop's information.
5
- def perform(shop, shop_data = nil)
5
+ def perform(_shop, shop_data = nil)
6
6
  # If we weren't provided with shop data (eg from a webhook), fetch it.
7
7
  shop_data ||= ActiveSupport::JSON::decode(ShopifyAPI::Shop.current.to_json)
8
8
 
@@ -1,7 +1,7 @@
1
1
  module DiscoApp::Concerns::SubscriptionChangedJob
2
2
  extend ActiveSupport::Concern
3
3
 
4
- def perform(shop, subscription)
4
+ def perform(_shop, subscription)
5
5
  DiscoApp::SendSubscriptionJob.perform_later(@shop)
6
6
  end
7
7
 
@@ -2,7 +2,7 @@ module DiscoApp::Concerns::SynchroniseCarrierServiceJob
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  # Ensure that any carrier service required by our app is registered.
5
- def perform(shop)
5
+ def perform(_shop)
6
6
  # Don't proceed unless we have a name and callback url.
7
7
  return unless carrier_service_name and callback_url
8
8
 
@@ -1,7 +1,7 @@
1
1
  module DiscoApp::Concerns::SynchroniseResourcesJob
2
2
  extend ActiveSupport::Concern
3
3
 
4
- def perform(shop, class_name, params)
4
+ def perform(_shop, class_name, params)
5
5
  klass = class_name.constantize
6
6
 
7
7
  klass::SHOPIFY_API_CLASS.find(:all, params: params).map do |shopify_resource|
@@ -0,0 +1,15 @@
1
+ module DiscoApp::Concerns::SynchroniseUsersJob
2
+ extend ActiveSupport::Concern
3
+
4
+ def perform(_shop)
5
+ begin
6
+ users = @shop.with_api_context {
7
+ ShopifyAPI::User.all
8
+ }
9
+ rescue ActiveResource::UnauthorizedAccess => e
10
+ Rollbar.error(e) and return
11
+ end
12
+ users.each { |user| DiscoApp::User.create_user(user, @shop) }
13
+ end
14
+
15
+ end
@@ -3,7 +3,7 @@ module DiscoApp::Concerns::SynchroniseWebhooksJob
3
3
 
4
4
  # Ensure the webhooks registered with our shop are the same as those listed
5
5
  # in our application configuration.
6
- def perform(shop)
6
+ def perform(_shop)
7
7
  # Get the full list of expected webhook topics.
8
8
  expected_topics = [:'app/uninstalled', :'shop/update'] + (DiscoApp.configuration.webhook_topics || [])
9
9
 
@@ -1,6 +1,6 @@
1
1
  class DiscoApp::SendSubscriptionJob < DiscoApp::ShopJob
2
2
 
3
- def perform(shop)
3
+ def perform(_shop)
4
4
  @shop.disco_api_client.create_app_subscription
5
5
  end
6
6
 
@@ -2,6 +2,9 @@
2
2
  # particular Shop's API session. The first argument to any job inheriting from
3
3
  # this class must be the domain of the relevant store, so that the appropriate
4
4
  # Shop model can be fetched and the temporary API session created.
5
+
6
+ require 'rollbar'
7
+
5
8
  class DiscoApp::ShopJob < ActiveJob::Base
6
9
 
7
10
  queue_as :default
@@ -19,9 +22,13 @@ class DiscoApp::ShopJob < ActiveJob::Base
19
22
  end
20
23
 
21
24
  def shop_context(job, block)
22
- @shop.with_api_context {
23
- block.call(job.arguments)
24
- }
25
+ Rollbar.scoped(rollbar_scope) do
26
+ @shop.with_api_context { block.call(job.arguments) }
27
+ end
28
+ end
29
+
30
+ def rollbar_scope
31
+ { person: { id: @shop.id, username: @shop.shopify_domain } }
25
32
  end
26
33
 
27
34
  end
@@ -0,0 +1,3 @@
1
+ class DiscoApp::SynchroniseUsersJob < DiscoApp::ShopJob
2
+ include DiscoApp::Concerns::SynchroniseUsersJob
3
+ end
@@ -9,6 +9,9 @@ module DiscoApp::Concerns::Shop
9
9
  has_many :subscriptions
10
10
  has_many :plans, through: :subscriptions
11
11
 
12
+ # Define relationship to users.
13
+ has_many :users
14
+
12
15
  # Define relationship to sessions.
13
16
  has_many :sessions, class_name: 'DiscoApp::Session', dependent: :destroy
14
17
 
@@ -19,6 +22,7 @@ module DiscoApp::Concerns::Shop
19
22
  scope :status, -> (status) { where status: status }
20
23
  scope :installed, -> { where status: statuses[:installed] }
21
24
  scope :has_active_shopify_plan, -> { where.not(plan_name: [:cancelled, :frozen, :fraudulent]) }
25
+ scope :shopify_plus, -> { where(plan_name: :shopify_plus) }
22
26
 
23
27
  # Alias 'with_shopify_session' as 'with_api_context' for better readability, but also as 'temp' for
24
28
  # backward compatibility.
@@ -27,7 +31,7 @@ module DiscoApp::Concerns::Shop
27
31
 
28
32
  # Return true if the shop is considered as in development mode.
29
33
  def development?
30
- ['staff', 'custom', 'affiliate'].include?(plan_name)
34
+ ['staff', 'affiliate'].include?(plan_name)
31
35
  end
32
36
 
33
37
  # Convenience method to check if this shop has a current subscription.
@@ -63,7 +67,7 @@ module DiscoApp::Concerns::Shop
63
67
 
64
68
  # Convenience method to get the email of the shop's admin, to display in Rollbar.
65
69
  def email
66
- self.data['email']
70
+ data[:email]
67
71
  end
68
72
 
69
73
  def installed_duration
@@ -74,7 +78,7 @@ module DiscoApp::Concerns::Shop
74
78
  # shop's "data" hash, return the default Rails zone (which should be UTC).
75
79
  def time_zone
76
80
  @time_zone ||= begin
77
- Time.find_zone!(data['timezone'].to_s.gsub(/^\(.+\)\s/, ''))
81
+ Time.find_zone!(data[:timezone].to_s.gsub(/^\(.+\)\s/, ''))
78
82
  rescue ArgumentError
79
83
  Time.zone
80
84
  end
@@ -83,7 +87,7 @@ module DiscoApp::Concerns::Shop
83
87
  # Return the shop's configured locale as a symbol. If none exists for some
84
88
  # reason, 'en' is returned.
85
89
  def locale
86
- (data['primary_locale'] || 'en').to_sym
90
+ (data[:primary_locale] || 'en').to_sym
87
91
  end
88
92
 
89
93
  # Return an instance of the Disco API client.
@@ -91,6 +95,11 @@ module DiscoApp::Concerns::Shop
91
95
  @api_client ||= DiscoApp::ApiClient.new(self, ENV['DISCO_API_URL'])
92
96
  end
93
97
 
98
+ # Override the "read" data attribute to allow indifferent access.
99
+ def data
100
+ read_attribute(:data).with_indifferent_access
101
+ end
102
+
94
103
  end
95
104
 
96
105
  end
@@ -0,0 +1,14 @@
1
+ module DiscoApp::Concerns::Source
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+
6
+ has_many :subscriptions
7
+ has_many :shops, through: :subscriptions
8
+
9
+ validates_presence_of :source
10
+ validates_presence_of :name
11
+
12
+ end
13
+
14
+ end
@@ -6,7 +6,7 @@ module DiscoApp::Concerns::Subscription
6
6
  belongs_to :shop
7
7
  belongs_to :plan
8
8
  belongs_to :plan_code
9
-
9
+ belongs_to :source
10
10
  has_many :one_time_charges, class_name: 'DiscoApp::ApplicationCharge', dependent: :destroy
11
11
  has_many :recurring_charges, class_name: 'DiscoApp::RecurringApplicationCharge', dependent: :destroy
12
12
 
@@ -58,4 +58,13 @@ module DiscoApp::Concerns::Synchronises
58
58
 
59
59
  end
60
60
 
61
+ included do
62
+
63
+ # Override the "read" data attribute to allow indifferent access.
64
+ def data
65
+ read_attribute(:data).with_indifferent_access
66
+ end
67
+
68
+ end
69
+
61
70
  end
@@ -2,15 +2,19 @@ module DiscoApp::Concerns::Taggable
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  def tags
5
- data['tags'].split(',').map(&:strip)
5
+ data[:tags].split(',').map(&:strip)
6
6
  end
7
7
 
8
8
  def add_tag(tag)
9
- data['tags'] = (tags + [tag]).uniq.join(',')
9
+ data[:tags] = (tags + [tag]).uniq.join(',')
10
10
  end
11
11
 
12
12
  def remove_tag(tag)
13
- data['tags'] = (tags - [tag]).uniq.join(',')
13
+ data[:tags] = (tags - [tag]).uniq.join(',')
14
+ end
15
+
16
+ def has_tag?(tag_to_check)
17
+ tags.any? { |tag| tag.casecmp(tag_to_check) }
14
18
  end
15
19
 
16
20
  end
@@ -0,0 +1,20 @@
1
+ module DiscoApp::Concerns::User
2
+ extend ActiveSupport::Concern
3
+
4
+ included do
5
+ belongs_to :shop
6
+
7
+ def self.create_user(shopify_user, shop)
8
+ user = self.find_or_create_by!(id: shopify_user.id, shop: shop)
9
+ user.update(
10
+ first_name: shopify_user.first_name || '',
11
+ last_name: shopify_user.last_name || '',
12
+ email: shopify_user.email
13
+ )
14
+ user
15
+ rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation
16
+ retry
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ class DiscoApp::Source < ActiveRecord::Base
2
+ include DiscoApp::Concerns::Source
3
+ end
@@ -0,0 +1,3 @@
1
+ class DiscoApp::User < ActiveRecord::Base
2
+ include DiscoApp::Concerns::User
3
+ end
@@ -46,19 +46,19 @@ module DiscoApp::Admin::Resources::Concerns::ShopResource
46
46
  end
47
47
 
48
48
  def email
49
- @model.data['email']
49
+ @model.data[:email]
50
50
  end
51
51
 
52
52
  def country_name
53
- @model.data['country_name']
53
+ @model.data[:country_name]
54
54
  end
55
55
 
56
56
  def currency
57
- @model.data['currency']
57
+ @model.data[:currency]
58
58
  end
59
59
 
60
60
  def plan_display_name
61
- @model.data['plan_display_name']
61
+ @model.data[:plan_display_name]
62
62
  end
63
63
 
64
64
  def current_subscription_id
@@ -2,7 +2,7 @@ class DiscoApp::SubscriptionService
2
2
 
3
3
  # Subscribe the given shop to the given plan, optionally using the given plan
4
4
  # code and optionally tracking the subscription source.
5
- def self.subscribe(shop, plan, plan_code = nil, source = nil)
5
+ def self.subscribe(shop, plan, plan_code = nil, source_name = nil)
6
6
 
7
7
  # If a plan code was provided, fetch it for the given plan.
8
8
  plan_code_instance = nil
@@ -10,6 +10,12 @@ class DiscoApp::SubscriptionService
10
10
  plan_code_instance = DiscoApp::PlanCode.available.find_by(plan: plan, code: plan_code)
11
11
  end
12
12
 
13
+ # If a source name has been provided, fetch or create it
14
+ source_instance = nil
15
+ if source_name.present?
16
+ source_instance = DiscoApp::Source.find_or_create_by(source: source_name)
17
+ end
18
+
13
19
  # Cancel any existing current subscriptions.
14
20
  shop.subscriptions.current.update_all(
15
21
  status: DiscoApp::Subscription.statuses[:cancelled],
@@ -33,7 +39,7 @@ class DiscoApp::SubscriptionService
33
39
  trial_period_days: plan.has_trial? ? subscription_trial_period_days : nil,
34
40
  trial_start_at: plan.has_trial? ? Time.now : nil,
35
41
  trial_end_at: plan.has_trial? ? subscription_trial_period_days.days.from_now : nil,
36
- source: source
42
+ source: source_instance
37
43
  )
38
44
 
39
45
  # Enqueue the subscription changed background job.