disco_app 0.18.1 → 0.18.4

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 (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
  }