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.
- 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