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.
- checksums.yaml +4 -4
- data/app/controllers/disco_app/charges_controller.rb +6 -1
- data/app/jobs/disco_app/concerns/synchronise_resources_job.rb +2 -6
- data/app/models/disco_app/concerns/has_metafields.rb +100 -43
- data/app/models/disco_app/concerns/synchronises.rb +1 -5
- data/app/models/disco_app/concerns/taggable.rb +1 -1
- data/app/services/disco_app/charges_service.rb +9 -11
- data/app/services/disco_app/synchronise_resources_service.rb +54 -0
- data/lib/disco_app/version.rb +1 -1
- data/lib/generators/disco_app/install/install_generator.rb +1 -1
- data/lib/generators/disco_app/install/templates/root/.gitignore +18 -2
- data/test/controllers/disco_app/charges_controller_test.rb +1 -2
- data/test/dummy/app/models/disco_app/shop.rb +3 -0
- data/test/dummy/package.json +4 -2
- data/test/dummy/yarn.lock +1989 -1955
- data/test/fixtures/api/widget_store/charges/{activate_application_charge_request.json → get_active_application_charge_response.json} +1 -1
- data/test/fixtures/api/widget_store/charges/{activate_recurring_application_charge_request.json → get_active_recurring_application_charge_response.json} +2 -2
- data/test/fixtures/api/widget_store/products/get_metafields_empty_response.json +3 -0
- data/test/fixtures/api/widget_store/products/get_metafields_with_existing_response.json +11 -0
- data/test/fixtures/api/widget_store/products/write_metafields_multiple_namespaces_request.json +4 -0
- data/test/fixtures/api/widget_store/products/write_metafields_single_namespace_request.json +2 -0
- data/test/fixtures/api/widget_store/products/write_metafields_with_existing_single_namespace_request.json +21 -0
- data/test/fixtures/api/widget_store/{charges/activate_application_charge_response.json → products/write_metafields_with_existing_single_namespace_response.json} +0 -0
- data/test/fixtures/api/widget_store/shops/get_metafields_with_existing_response.json +11 -0
- data/test/fixtures/api/widget_store/shops/write_metafields_with_existing_first_request.json +9 -0
- data/test/fixtures/api/widget_store/{charges/activate_recurring_application_charge_response.json → shops/write_metafields_with_existing_first_response.json} +0 -0
- data/test/fixtures/api/widget_store/shops/write_metafields_with_existing_second_request.json +9 -0
- data/test/fixtures/api/widget_store/shops/write_metafields_with_existing_second_response.json +1 -0
- data/test/models/disco_app/has_metafields_test.rb +72 -2
- data/test/services/disco_app/charges_service_test.rb +3 -6
- data/test/services/disco_app/synchronise_resources_service_test.rb +57 -0
- data/test/test_helper.rb +2 -0
- data/test/vcr/synchronise_products.yml +130 -0
- data/test/vcr/synchronise_products_paginated.yml +119 -0
- data/test/vcr/synchronise_products_since_id.yml +125 -0
- data/test/vcr/synchronise_products_with_params.yml +130 -0
- metadata +51 -16
- data/test/fixtures/api/widget_store/charges/get_accepted_application_charge_response.json +0 -16
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a75590acf98965732300783166f37bc5b7424d85e39b602831f6d79ac496cf9b
|
4
|
+
data.tar.gz: 4ef13a5f01e043a3464698539245bd4982a0bb82c4a20a7c4081176f795f30b6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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(
|
6
|
-
|
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
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
metafields
|
28
|
-
|
29
|
-
|
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
|
-
|
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
|
|
@@ -32,8 +32,13 @@ class DiscoApp::ChargesService
|
|
32
32
|
charge
|
33
33
|
end
|
34
34
|
|
35
|
-
#
|
36
|
-
#
|
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
|
47
|
-
return false unless charge.
|
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
|
data/lib/disco_app/version.rb
CHANGED
@@ -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/
|
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 }
|
data/test/dummy/package.json
CHANGED