disco_app 0.18.1 → 0.18.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/disco_app/charges_controller.rb +6 -1
  3. data/app/jobs/disco_app/concerns/synchronise_resources_job.rb +2 -6
  4. data/app/models/disco_app/concerns/has_metafields.rb +100 -43
  5. data/app/models/disco_app/concerns/synchronises.rb +1 -5
  6. data/app/models/disco_app/concerns/taggable.rb +1 -1
  7. data/app/services/disco_app/charges_service.rb +9 -11
  8. data/app/services/disco_app/synchronise_resources_service.rb +54 -0
  9. data/lib/disco_app/version.rb +1 -1
  10. data/lib/generators/disco_app/install/install_generator.rb +1 -1
  11. data/lib/generators/disco_app/install/templates/root/.gitignore +18 -2
  12. data/test/controllers/disco_app/charges_controller_test.rb +1 -2
  13. data/test/dummy/app/models/disco_app/shop.rb +3 -0
  14. data/test/dummy/package.json +4 -2
  15. data/test/dummy/yarn.lock +1989 -1955
  16. data/test/fixtures/api/widget_store/charges/{activate_application_charge_request.json → get_active_application_charge_response.json} +1 -1
  17. data/test/fixtures/api/widget_store/charges/{activate_recurring_application_charge_request.json → get_active_recurring_application_charge_response.json} +2 -2
  18. data/test/fixtures/api/widget_store/products/get_metafields_empty_response.json +3 -0
  19. data/test/fixtures/api/widget_store/products/get_metafields_with_existing_response.json +11 -0
  20. data/test/fixtures/api/widget_store/products/write_metafields_multiple_namespaces_request.json +4 -0
  21. data/test/fixtures/api/widget_store/products/write_metafields_single_namespace_request.json +2 -0
  22. data/test/fixtures/api/widget_store/products/write_metafields_with_existing_single_namespace_request.json +21 -0
  23. data/test/fixtures/api/widget_store/{charges/activate_application_charge_response.json → products/write_metafields_with_existing_single_namespace_response.json} +0 -0
  24. data/test/fixtures/api/widget_store/shops/get_metafields_with_existing_response.json +11 -0
  25. data/test/fixtures/api/widget_store/shops/write_metafields_with_existing_first_request.json +9 -0
  26. data/test/fixtures/api/widget_store/{charges/activate_recurring_application_charge_response.json → shops/write_metafields_with_existing_first_response.json} +0 -0
  27. data/test/fixtures/api/widget_store/shops/write_metafields_with_existing_second_request.json +9 -0
  28. data/test/fixtures/api/widget_store/shops/write_metafields_with_existing_second_response.json +1 -0
  29. data/test/models/disco_app/has_metafields_test.rb +72 -2
  30. data/test/services/disco_app/charges_service_test.rb +3 -6
  31. data/test/services/disco_app/synchronise_resources_service_test.rb +57 -0
  32. data/test/test_helper.rb +2 -0
  33. data/test/vcr/synchronise_products.yml +130 -0
  34. data/test/vcr/synchronise_products_paginated.yml +119 -0
  35. data/test/vcr/synchronise_products_since_id.yml +125 -0
  36. data/test/vcr/synchronise_products_with_params.yml +130 -0
  37. metadata +51 -16
  38. data/test/fixtures/api/widget_store/charges/get_accepted_application_charge_response.json +0 -16
  39. data/test/fixtures/api/widget_store/charges/get_accepted_recurring_application_charge_response.json +0 -20
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e64f5353b2ff61d1c3dfb3687437e41a5b0bcd95341997a50a94a1f514b9b87
4
- data.tar.gz: 995cdd1d825a71b2a27e0fb9bee55bd95a24b5b89967e1719875c413a1882dca
3
+ metadata.gz: a75590acf98965732300783166f37bc5b7424d85e39b602831f6d79ac496cf9b
4
+ data.tar.gz: 4ef13a5f01e043a3464698539245bd4982a0bb82c4a20a7c4081176f795f30b6
5
5
  SHA512:
6
- metadata.gz: b5195516feed35026a348bf3842c64e9fd7235a25eaf6544dad2b69ba7be8fdf885bf731bba300cdfe6dcea1f76a2d37da1c75d315b45d6fb62d68791a3d7bc4
7
- data.tar.gz: e43fcbd82c8b9804e3cb574436bf406fcede8b12c3dc9ae7e65323ab8c4bdb5b8b605162f3efc036fd37ad7a1d22fad817e2a52b585a43f3ca718cd89cec38e1
6
+ metadata.gz: e1179214f99827a732d322c1361707625578872484baa6430a946f887750d6b5b6d299c7c0558505d743993dc38db713ee1853338cd2ebd8552d391968d761a8
7
+ data.tar.gz: d835598d84013984dfa24c565e8868e21225b166dab8ca9ea09d05b7907568ec71d8eba4ab846cc6af5f03459df82e31786094a3b5fc7f1d438469e0c2ac7138
@@ -21,9 +21,14 @@ class DiscoApp::ChargesController < ApplicationController
21
21
  end
22
22
  end
23
23
 
24
- # Attempt to activate a charge after a user has accepted or declined it.
24
+ # Attempt to activate a charge (locally) after a user has accepted or declined it.
25
25
  # Redirect to the main application's root URL immediately afterwards - if the
26
26
  # charge wasn't accepted, the flow will start again.
27
+ #
28
+ # Previously, the activation of a charge also required updating Shopify via the
29
+ # API, but that requirement has been removed.
30
+ #
31
+ # See https://shopify.dev/changelog/auto-activation-of-charges-and-subscriptions
27
32
  def activate
28
33
  # First attempt to find a matching charge.
29
34
  if (charge = @subscription.charges.find_by(id: params[:id], shopify_id: params[:charge_id])).nil?
@@ -2,12 +2,8 @@ module DiscoApp::Concerns::SynchroniseResourcesJob
2
2
 
3
3
  extend ActiveSupport::Concern
4
4
 
5
- def perform(_shop, class_name, params)
6
- klass = class_name.constantize
7
-
8
- klass::SHOPIFY_API_CLASS.find(:all, params: params).map do |shopify_resource|
9
- klass.synchronise(@shop, shopify_resource.serializable_hash)
10
- end
5
+ def perform(shop, class_name, params, since_id = 0)
6
+ DiscoApp::SynchroniseResourcesService.synchronise_all(shop, class_name, params, since_id)
11
7
  end
12
8
 
13
9
  end
@@ -1,47 +1,104 @@
1
- module DiscoApp::Concerns::HasMetafields
2
-
3
- extend ActiveSupport::Concern
4
-
5
- included do
6
- # Write multiple metafields for this object in a single call.
7
- #
8
- # Expects an argument in a nested hash structure with :namespace => :key =>
9
- # :value, eg:
10
- #
11
- # Product.write_metafields(myapp: {
12
- # key1: 'value1',
13
- # key2: 'value2'
14
- # })
15
- #
16
- # This method assumes that it is being called within a valid Shopify API
17
- # session context, eg @shop.with_api_context { ... }.
18
- #
19
- # It also assumes that the including class has defined the appropriate value
20
- # for SHOPIFY_API_CLASS and that calling the `id` method on the instance
21
- # will return the relevant object's Shopify ID.
22
- #
23
- # Returns true on success, false otherwise.
24
- def write_metafields(metafields)
25
- self.class::SHOPIFY_API_CLASS.new(
26
- id: id,
27
- metafields: build_metafields(metafields)
28
- ).save
29
- end
1
+ module DiscoApp
2
+ module Concerns
3
+ module HasMetafields
4
+
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ # Write multiple metafields for this object in a single call.
9
+ #
10
+ # Expects an argument in a nested hash structure with :namespace => :key => :value, eg:
11
+ #
12
+ # Product.write_metafields(myapp: {
13
+ # key1: 'value1',
14
+ # key2: 3,
15
+ # key3: { 'value_key' => 'value_value' }
16
+ # })
17
+ #
18
+ # String, integer and hash values will have their value type detected and set accordingly.
19
+ #
20
+ # This method assumes that it is being called within a valid Shopify API session context,
21
+ # eg @shop.with_api_context { ... }.
22
+ #
23
+ # It also assumes that the including class has defined the appropriate value for
24
+ # SHOPIFY_API_CLASS.
25
+ #
26
+ # To avoid an issue with trying to set metafield values for namespace and key pairs that
27
+ # already exist, this method also performs a lookup of existing metafields as part of the
28
+ # write process and ensures we set the corresponding metafield ID on the update call if
29
+ # needed.
30
+ #
31
+ # Returns true on success, raises an exception otherwise.
32
+ def write_metafields(metafields)
33
+ return write_shop_metafields(metafields) if shopify_api_class_is_shop?
34
+
35
+ write_resource_metafields(metafields)
36
+ end
37
+
38
+ # Writing shop metafields is a special case - they need to be saved one by one.
39
+ def write_shop_metafields(metafields)
40
+ build_metafields(metafields).all?(&:save!)
41
+ end
42
+
43
+ # Writing regular resource metafields can be done in a single request.
44
+ def write_resource_metafields(metafields)
45
+ self.class::SHOPIFY_API_CLASS.new(
46
+ id: id,
47
+ metafields: build_metafields(metafields)
48
+ ).save!
49
+ end
50
+
51
+ # Give a nested hash of metafields in the format described above, return an array of
52
+ # corresponding ShopifyAPI::Metafield instances.
53
+ def build_metafields(metafields)
54
+ metafields.flat_map do |namespace, keys|
55
+ keys.map do |key, value|
56
+ ShopifyAPI::Metafield.new(
57
+ id: existing_metafield_id(namespace, key),
58
+ namespace: namespace,
59
+ key: key,
60
+ value: build_value(value),
61
+ value_type: build_value_type(value)
62
+ )
63
+ end
64
+ end
65
+ end
66
+
67
+ # Return the metafield value based on the provided value.
68
+ def build_value(value)
69
+ return value.to_json if value.is_a?(Hash)
70
+
71
+ value
72
+ end
73
+
74
+ # Return the metafield type based on the provided value type.
75
+ def build_value_type(value)
76
+ return :json_string if value.is_a?(Hash)
77
+ return :integer if value.is_a?(Integer)
78
+
79
+ :string
80
+ end
81
+
82
+ # Given a namespace and key, attempt to find the ID of a corresponding metafield in the
83
+ # given set of existing metafields.
84
+ def existing_metafield_id(namespace, key)
85
+ existing_metafields.find do |existing_metafield|
86
+ (existing_metafield.namespace.to_sym == namespace.to_sym) && (existing_metafield.key.to_sym == key.to_sym)
87
+ end&.id
88
+ end
89
+
90
+ # Fetch and cache existing metafields for this object from the Shopify API.
91
+ def existing_metafields
92
+ @existing_metafields ||= begin
93
+ self.class::SHOPIFY_API_CLASS.new(id: id).metafields
94
+ end
95
+ end
96
+
97
+ def shopify_api_class_is_shop?
98
+ self.class::SHOPIFY_API_CLASS == ShopifyAPI::Shop
99
+ end
100
+ end
30
101
 
31
- # Give a nested hash of metafields in the format described above, return
32
- # an array of corresponding ShopifyAPI::Metafield instances.
33
- def build_metafields(metafields)
34
- metafields.map do |namespace, keys|
35
- keys.map do |key, value|
36
- ShopifyAPI::Metafield.new(
37
- namespace: namespace,
38
- key: key,
39
- value: value,
40
- value_type: value.is_a?(Integer) ? :integer : :string
41
- )
42
- end
43
- end.flatten
44
102
  end
45
103
  end
46
-
47
104
  end
@@ -47,11 +47,7 @@ module DiscoApp::Concerns::Synchronises
47
47
  end
48
48
 
49
49
  def synchronise_all(shop, params = {})
50
- resource_count = shop.with_api_context { self::SHOPIFY_API_CLASS.count(params) }
51
-
52
- (1..(resource_count / SYNCHRONISES_PAGE_LIMIT.to_f).ceil).each do |page|
53
- DiscoApp::SynchroniseResourcesJob.perform_later(shop, name, params.merge(page: page, limit: SYNCHRONISES_PAGE_LIMIT))
54
- end
50
+ DiscoApp::SynchroniseResourcesJob.perform_later(shop, name, params)
55
51
  end
56
52
  end
57
53
 
@@ -3,7 +3,7 @@ module DiscoApp::Concerns::Taggable
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  def tags
6
- data[:tags].split(',').map(&:strip)
6
+ data[:tags].to_s.split(',').map(&:strip)
7
7
  end
8
8
 
9
9
  def add_tag(tag)
@@ -32,8 +32,13 @@ class DiscoApp::ChargesService
32
32
  charge
33
33
  end
34
34
 
35
- # Attempt to activate the given Shopify charge for the given Shop using the
36
- # Shopify API. Returns true on successful activation, false otherwise.
35
+ # Synchronises the status of a given charge from the Shopify API and returns
36
+ # true if it's active (and false otherwise).
37
+ #
38
+ # Previously, the activation of a charge also required updating Shopify via the
39
+ # API, but that requirement has been removed.
40
+ #
41
+ # See https://shopify.dev/changelog/auto-activation-of-charges-and-subscriptions
37
42
  def self.activate(shop, charge)
38
43
  # Start by fetching the Shopify charge to check that it was accepted.
39
44
  shopify_charge = shop.with_api_context do
@@ -43,20 +48,13 @@ class DiscoApp::ChargesService
43
48
  # Update the status of the local charge based on the Shopify charge.
44
49
  charge.send("#{shopify_charge.status}!") if charge.respond_to? "#{shopify_charge.status}!"
45
50
 
46
- # If the charge wasn't accepted, fail and return.
47
- return false unless charge.accepted?
48
-
49
- # If the charge was indeed accepted, activate it via Shopify.
50
- charge.shop.with_api_context do
51
- shopify_charge.activate
52
- end
51
+ # If the charge isn't active, fail and return.
52
+ return false unless charge.active?
53
53
 
54
54
  # If the charge was recurring, make sure that all other local recurring
55
55
  # charges are marked inactive.
56
56
  cancel_recurring_charges(shop, charge) if charge.recurring?
57
57
 
58
- charge.active!
59
-
60
58
  true
61
59
  rescue StandardError
62
60
  false
@@ -0,0 +1,54 @@
1
+ module DiscoApp
2
+ class SynchroniseResourcesService
3
+
4
+ PAGE_LIMIT = 250
5
+
6
+ attr_reader :shop, :class_name, :params, :since_id
7
+
8
+ def self.synchronise_all(shop, class_name, params, since_id = 0)
9
+ new(shop, class_name, params, since_id).synchronise_all
10
+ end
11
+
12
+ def initialize(shop, class_name, params, since_id)
13
+ @shop = shop
14
+ @class_name = class_name
15
+ @params = params
16
+ @since_id = since_id
17
+ end
18
+
19
+ def synchronise_all
20
+ synchronise_page
21
+ finish_or_loop
22
+ end
23
+
24
+ private
25
+
26
+ attr_reader :shopify_records
27
+
28
+ def synchronise_page(_last_shopify_record = since_id)
29
+ request_params = params.merge({
30
+ limit: PAGE_LIMIT,
31
+ since_id: since_id
32
+ })
33
+
34
+ @shopify_records = shop.with_api_context do
35
+ synchronise_class::SHOPIFY_API_CLASS.find(:all, params: request_params)
36
+ end
37
+
38
+ shopify_records.each do |shopify_record|
39
+ synchronise_class.synchronise(shop, shopify_record)
40
+ end
41
+ end
42
+
43
+ def finish_or_loop
44
+ return if shopify_records.empty? || shopify_records.size < PAGE_LIMIT
45
+
46
+ DiscoApp::SynchroniseResourcesJob.perform_later(shop, class_name, params, shopify_records.last.id)
47
+ end
48
+
49
+ def synchronise_class
50
+ @synchronise_class ||= class_name.constantize
51
+ end
52
+
53
+ end
54
+ end
@@ -1,5 +1,5 @@
1
1
  module DiscoApp
2
2
 
3
- VERSION = '0.18.1'.freeze
3
+ VERSION = '0.18.4'.freeze
4
4
 
5
5
  end
@@ -39,7 +39,7 @@ module DiscoApp
39
39
  gem 'classnames-rails'
40
40
  gem 'nokogiri'
41
41
  gem 'oj'
42
- gem 'pg'
42
+ gem 'pg', '~> 1.1'
43
43
  gem 'premailer-rails'
44
44
  gem 'react-rails'
45
45
  gem 'render_anywhere'
@@ -2,8 +2,10 @@
2
2
  *.pgdump
3
3
  capybara-*.html
4
4
  .rspec
5
- /log
6
- /tmp
5
+ /log/*
6
+ /tmp/*
7
+ !/log/.keep
8
+ !/tmp/.keep
7
9
  /db/*.sqlite3
8
10
  /db/*.sqlite3-journal
9
11
  /public/system
@@ -17,6 +19,7 @@ pickle-email-*.html
17
19
  .disco_app
18
20
  /public/packs
19
21
  /public/packs-test
22
+ /public/assets
20
23
  /node_modules
21
24
  yarn-debug.log*
22
25
  .yarn-integrity
@@ -29,6 +32,18 @@ yarn-debug.log*
29
32
  .envrc
30
33
  .pryrc
31
34
 
35
+ # Ignore pidfiles, but keep the directory.
36
+ /tmp/pids/*
37
+ !/tmp/pids/
38
+ !/tmp/pids/.keep
39
+
40
+ # Ignore uploaded files in development.
41
+ /storage/*
42
+ !/storage/.keep
43
+
44
+ # Ignore master key for decrypting credentials and more.
45
+ /config/master.key
46
+
32
47
  # these should all be checked in to normalise the environment:
33
48
  # Gemfile.lock, .ruby-version, .ruby-gemset
34
49
 
@@ -37,6 +52,7 @@ yarn-debug.log*
37
52
 
38
53
  # if using bower-rails ignore default bower_components path bower.json files
39
54
  /vendor/assets/bower_components
55
+
40
56
  *.bowerrc
41
57
  bower.json
42
58
 
@@ -86,8 +86,7 @@ class DiscoApp::ChargesControllerTest < ActionController::TestCase
86
86
  end
87
87
 
88
88
  test 'user trying to activate accepted charge succeeds and is redirected to the root page' do
89
- stub_api_request(:get, "#{@shop.admin_url}/recurring_application_charges/654381179.json", 'widget_store/charges/get_accepted_recurring_application_charge')
90
- stub_api_request(:post, "#{@shop.admin_url}/recurring_application_charges/654381179/activate.json", 'widget_store/charges/activate_recurring_application_charge')
89
+ stub_api_request(:get, "#{@shop.admin_url}/recurring_application_charges/654381179.json", 'widget_store/charges/get_active_recurring_application_charge')
91
90
 
92
91
  @current_subscription.active_charge.destroy
93
92
  get :activate, params: { subscription_id: @current_subscription, id: @new_charge.id, charge_id: @new_charge.shopify_id }
@@ -4,6 +4,9 @@ class DiscoApp::Shop < ApplicationRecord
4
4
 
5
5
  include DiscoApp::Concerns::Shop
6
6
 
7
+ include DiscoApp::Concerns::HasMetafields
8
+ SHOPIFY_API_CLASS = ShopifyAPI::Shop
9
+
7
10
  has_one :js_configuration
8
11
  has_one :widget_configuration
9
12
  has_many :carts
@@ -1,8 +1,10 @@
1
1
  {
2
2
  "dependencies": {
3
- "@rails/webpacker": "^4.0.7"
3
+ "@rails/webpacker": "5.4.3",
4
+ "webpack": "^4.46.0",
5
+ "webpack-cli": "^3.3.12"
4
6
  },
5
7
  "devDependencies": {
6
- "webpack-dev-server": "^3.9.0"
8
+ "webpack-dev-server": "^3"
7
9
  }
8
10
  }