lucid-shopify 0.34.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 217e5561d29c394e5975a12714a48d9dd4d5a4e93b7e69d421ec2636870ac7db
4
+ data.tar.gz: fc995f74c559fd840abf1a865b1dc387b836dcfa75ac4c1024c70671f27fbe2f
5
+ SHA512:
6
+ metadata.gz: fbd32aef35360dcfd9da86a985d1cbbfb459c0443ea968dd8ca08645560bf4257d3839fe457b0a298080e901c1bab10dfecf5a2e1c2ccce931cc7b7bc27db76d
7
+ data.tar.gz: a5019bb0890375bb869195ffda3c8e2d41b0ba80339dfd47ac16fe4637f6f3319804698dfd6d035b8e2d68ac9281a6de977129d4c8c63b37c40a77cfb546ea61
data/README.md ADDED
@@ -0,0 +1,156 @@
1
+ lucid-shopify
2
+ =============
3
+
4
+ Installation
5
+ ------------
6
+
7
+ Add the gem to your ‘Gemfile’:
8
+
9
+ gem 'lucid-shopify'
10
+
11
+
12
+ Usage
13
+ -----
14
+
15
+ ### Configure the default API client
16
+
17
+ Lucid::Shopify.configure(
18
+ api_key: '...',
19
+ api_version: '...', # e.g. '2019-07'
20
+ billing_callback_uri: '...',
21
+ callback_uri: '...', # (for OAuth; unused by this gem)
22
+ logger: Logger.new(STDOUT),
23
+ scope: '...',
24
+ shared_secret: '...',
25
+ webhook_uri: '...',
26
+ )
27
+
28
+ Alternatively load the configuration from a Ruby file. The Ruby
29
+ file is evaluated and should return a hash.
30
+
31
+ Lucid::Shopify.configure_from_file('config/shopify.rb') # the default path
32
+
33
+ When loading from a file, any environment variables matching the
34
+ upcased key with the prefix ‘SHOPIFY_’ will override values in the
35
+ file. For example ‘SHOPIFY_SHARED_SECRET=...’.
36
+
37
+ All keys are optional and in some private apps, you may not require
38
+ any configuration at all.
39
+
40
+ Additionally, each API request requires authorisation:
41
+
42
+ credentials = Lucid::Shopify::Credentials.new(
43
+ '...', # myshopify_domain
44
+ '...', # access_token
45
+ )
46
+
47
+ If the access token is omitted, the request will be unauthorised.
48
+ This is only useful during the OAuth2 process.
49
+
50
+
51
+ ### Configure webhooks
52
+
53
+ Configure each webhook the app will create (if any):
54
+
55
+ webhooks = Lucid::Shopify::Container['webhook_list']
56
+
57
+ webhooks.register('orders/create', fields: 'id,tags'}
58
+
59
+
60
+ ### Register webhook handlers
61
+
62
+ For each webhook, register one or more handlers:
63
+
64
+ handlers = Lucid::Shopify::Container['webhook_handler_list']
65
+
66
+ handlers.register('orders/create', OrdersCreateWebhook.new)
67
+
68
+ See the inline method documentation for more detail.
69
+
70
+ To call/delegate a webhook to its handler for processing, you will likely want
71
+ to create a worker around something like this:
72
+
73
+ webhook = Lucid::Shopify::Webhook.new(myshopify_domain, topic, data)
74
+
75
+ handlers.delegate(webhook)
76
+
77
+
78
+ ### Create and delete webhooks
79
+
80
+ Create/delete all configured webhooks (see above):
81
+
82
+ Lucid::Shopify::CreateAllWebhooks.new.(credentials)
83
+ Lucid::Shopify::DeleteAllWebhooks.new.(credentials)
84
+
85
+ Create/delete webhooks manually:
86
+
87
+ webhook = {topic: 'orders/create', fields: %w(id tags)}
88
+
89
+ Lucid::Shopify::CreateWebhook.new.(credentials, webhook)
90
+ Lucid::Shopify::DeleteWebhook.new.(credentials, webhook_id)
91
+
92
+
93
+ ### Verification
94
+
95
+ Verify callback requests with the request params:
96
+
97
+ begin
98
+ Lucid::Shopify::VerifyCallback.new.(params)
99
+ rescue Lucid::Shopify::Error => e
100
+ # ...
101
+ end
102
+
103
+ Verify webhook requests with the request data and the HMAC header:
104
+
105
+ begin
106
+ Lucid::Shopify::VerifyWebhook.new.(data, hmac)
107
+ rescue Lucid::Shopify::Error => e
108
+ # ...
109
+ end
110
+
111
+
112
+ ### Authorisation
113
+
114
+ authorise = Lucid::Shopify::Authorise.new
115
+
116
+ access_token = authorise.(credentials, authorisation_code)
117
+
118
+
119
+ ### Billing
120
+
121
+ Create a new charge:
122
+
123
+ create_charge = Lucid::Shopify::CreateCharge.new
124
+
125
+ charge = create_charge.(credentials, charge) # see Lucid::Shopify::Charge
126
+
127
+ Redirect the user to `charge['confirmation_url']`. When the user
128
+ returns (see `config.billing_callback_uri`), activate the accepted
129
+ charge:
130
+
131
+ activate_charge = Lucid::Shopify::ActivateCharge.new
132
+
133
+ activate_charge.(credentials, accepted_charge)
134
+
135
+
136
+ ### Make API requests
137
+
138
+ client = Lucid::Shopify::Client.new
139
+
140
+ client.get(credentials, 'orders', since_id: since_id)['orders']
141
+ client.post_json(credentials, 'orders', new_order)
142
+
143
+ Request logging is disabled by default. To enable it:
144
+
145
+ Lucid::Shopify.configure(
146
+ logger: Logger.new(STDOUT),
147
+ )
148
+
149
+
150
+ ### Make throttled API requests
151
+
152
+ client.throttled.get(credentials, 'orders')
153
+ client.throttled.post_json(credentials, 'orders', new_order)
154
+
155
+ Note that throttling currently uses a naive implementation that is
156
+ only maintained across a single thread.
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lucid/shopify/container'
4
+
5
+ module Lucid
6
+ module Shopify
7
+ class ActivateCharge
8
+ # @param client [#post_json]
9
+ def initialize(client: Container[:client])
10
+ @client = client
11
+ end
12
+
13
+ # Activate a recurring application charge.
14
+ #
15
+ # @param credentials [Credentials]
16
+ # @param charge [#to_h] an accepted charge received from Shopify via callback
17
+ #
18
+ # @return [Hash] the active charge
19
+ def call(credentials, charge)
20
+ data = @client.post_json(credentials, "recurring_application_charges/#{charge.to_h['id']}/activate", charge.to_h)
21
+
22
+ data['recurring_application_charge']
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lucid/shopify/container'
4
+
5
+ module Lucid
6
+ module Shopify
7
+ class Authorise
8
+ Error = Class.new(Error)
9
+
10
+ # @param client [#post_json]
11
+ def initialize(client: Container[:client])
12
+ @client = client
13
+ end
14
+
15
+ # Exchange an authorisation code for a new Shopify access token.
16
+ #
17
+ # @param myshopify_domain [String]
18
+ # @param authorisation_code [String]
19
+ #
20
+ # @return [String] the access token
21
+ #
22
+ # @raise [Error] if the response is invalid
23
+ def call(myshopify_domain, authorisation_code)
24
+ credentials = Credentials.new(myshopify_domain)
25
+
26
+ data = @client.post_json(credentials, 'oauth/access_token', post_data(authorisation_code))
27
+
28
+ raise Error if data['access_token'].nil?
29
+ raise Error if data['scope'] != Shopify.config.scope
30
+
31
+ data['access_token']
32
+ end
33
+
34
+ # @param authorisation_code [String]
35
+ #
36
+ # @return [Hash]
37
+ private def post_data(authorisation_code)
38
+ {
39
+ client_id: Shopify.config.api_key,
40
+ client_secret: Shopify.config.shared_secret,
41
+ code: authorisation_code,
42
+ }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lucid/shopify/container'
4
+
5
+ %w[delete get post put].each { |m| require "lucid/shopify/#{m}_request" }
6
+
7
+ module Lucid
8
+ module Shopify
9
+ class Client
10
+ # @param send_request [#call]
11
+ # @param send_throttled_request [#call]
12
+ def initialize(send_request: Container[:send_request],
13
+ send_throttled_request: Container[:send_throttled_request],
14
+ throttling: false)
15
+ @send_request = send_request
16
+ @send_throttled_request = send_throttled_request
17
+ @throttling = throttling
18
+
19
+ @params = {
20
+ send_request: @send_request,
21
+ send_throttled_request: @send_throttled_request
22
+ }
23
+ end
24
+
25
+ # @return [#call]
26
+ private def send_request
27
+ throttled? ? @send_throttled_request : @send_request
28
+ end
29
+
30
+ # @return [Boolean]
31
+ def throttled?
32
+ @throttling
33
+ end
34
+
35
+ # Returns a new instance with throttling enabled, or self.
36
+ #
37
+ # @return [Client, self]
38
+ def throttled
39
+ return self if throttled?
40
+
41
+ self.class.new(**@params, throttling: true)
42
+ end
43
+
44
+ # Returns a new instance with throttling disabled, or self.
45
+ #
46
+ # @return [Client, self]
47
+ def unthrottled
48
+ return self unless throttled?
49
+
50
+ self.class.new(**@params, throttling: false)
51
+ end
52
+
53
+ # @see DeleteRequest#initialize
54
+ def delete(*args)
55
+ send_request.(DeleteRequest.new(*args))
56
+ end
57
+
58
+ # @see GetRequest#initialize
59
+ def get(*args)
60
+ send_request.(GetRequest.new(*args))
61
+ end
62
+
63
+ # @see PostRequest#initialize
64
+ def post_json(*args)
65
+ send_request.(PostRequest.new(*args))
66
+ end
67
+
68
+ # @see PutRequest#initialize
69
+ def put_json(*args)
70
+ send_request.(PutRequest.new(*args))
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ require 'lucid/shopify'
6
+ require 'lucid/utils'
7
+
8
+ module Lucid
9
+ module Shopify
10
+ class << self
11
+ # @param options [Hash]
12
+ #
13
+ # @return [Config]
14
+ def configure(options = {})
15
+ @config = Config.new(
16
+ **@config.to_h.compact,
17
+ **options,
18
+ )
19
+ end
20
+
21
+ # @param path [String]
22
+ #
23
+ # @return [Config]
24
+ def configure_from_file(path = 'config/shopify.rb')
25
+ options = Utils::ConfigFromFile.new.(path, env_prefix: 'shopify')
26
+
27
+ configure(options)
28
+ end
29
+
30
+ # @return [Config]
31
+ def config
32
+ @config ||= configure
33
+ end
34
+ end
35
+
36
+ class Config < Dry::Struct
37
+ attribute :api_version, Types::String.default('2019-07')
38
+ attribute :logger, Types::Logger.default(Logger.new(File::NULL).freeze)
39
+
40
+ # The following attributes may be unnecessary in some private apps.
41
+ attribute? :api_key, Types::String
42
+ attribute? :billing_callback_uri, Types::String
43
+ attribute? :callback_uri, Types::String
44
+ attribute? :scope, Types::String
45
+ attribute? :shared_secret, Types::String
46
+ attribute? :webhook_uri, Types::String
47
+ end
48
+
49
+ self.configure
50
+ end
51
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/container'
4
+ require 'http'
5
+
6
+ require 'lucid/shopify'
7
+ require 'lucid/shopify/config'
8
+
9
+ module Lucid
10
+ module Shopify
11
+ Container = Dry::Container.new
12
+
13
+ # Services only (dependencies); no value objects, entities.
14
+ Container.register(:activate_charge) { ActivateCharge.new }
15
+ Container.register(:authorise) { Authorise.new }
16
+ Container.register(:client) { Client.new }
17
+ Container.register(:create_all_webhooks) { CreateAllWebhooks.new }
18
+ Container.register(:create_charge) { CreateCharge.new }
19
+ Container.register(:create_webhook) { CreateWebhook.new }
20
+ Container.register(:delete_all_webhooks) { DeleteAllWebhooks.new }
21
+ Container.register(:delete_webhook) { DeleteWebhook.new }
22
+ Container.register(:http) { ::HTTP::Client.new }
23
+ Container.register(:send_request) { SendRequest.new }
24
+ Container.register(:send_throttled_request) do
25
+ if defined?(Redis)
26
+ SendRequest.new(strategy: RedisThrottledStrategy.new)
27
+ else
28
+ SendRequest.new(strategy: ThrottledStrategy.new)
29
+ end
30
+ end
31
+ Container.register(:verify_callback) { VerifyCallback.new }
32
+ Container.register(:verify_webhook) { VerifyWebhook.new }
33
+ Container.register(:webhook_handler_list) { Shopify.handlers }
34
+ Container.register(:webhook_list) { Shopify.webhooks }
35
+ end
36
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lucid/shopify/container'
4
+
5
+ module Lucid
6
+ module Shopify
7
+ class CreateAllWebhooks
8
+ # @param create_webhook [#call]
9
+ def initialize(create_webhook: Container[:create_webhook])
10
+ @create_webhook = create_webhook
11
+ end
12
+
13
+ # Create all webhooks for the shop. Shopify ignores any webhooks which
14
+ # already exist remotely.
15
+ #
16
+ # @param credentials [Credentials]
17
+ # @param webhooks [WebhookList]
18
+ #
19
+ # @return [Array<Hash>] response data
20
+ def call(credentials, webhooks: Container[:webhook_list])
21
+ webhooks.map do |webhook|
22
+ Thread.new { @create_webhook.(credentials, webhook) }
23
+ end.map(&:value)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lucid/shopify/container'
4
+
5
+ module Lucid
6
+ module Shopify
7
+ class CreateCharge
8
+ # @param client [#post_json]
9
+ def initialize(client: Container[:client])
10
+ @client = client
11
+ end
12
+
13
+ # Create a new recurring application charge.
14
+ #
15
+ # @param credentials [Credentials]
16
+ # @param charge [Hash]
17
+ #
18
+ # @return [Hash] the pending charge
19
+ def call(credentials, charge)
20
+ data = @client.post_json(credentials, 'recurring_application_charges', post_data(charge))
21
+
22
+ data['recurring_application_charge']
23
+ end
24
+
25
+ # @param charge [Hash]
26
+ #
27
+ # @return [Hash]
28
+ private def post_data(charge)
29
+ {
30
+ 'recurring_application_charge' => {
31
+ 'return_url' => Shopify.config.billing_callback_uri
32
+ }.merge(charge.transform_keys(&:to_s)),
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lucid/shopify/container'
4
+
5
+ module Lucid
6
+ module Shopify
7
+ class CreateWebhook
8
+ # @param client [#post_json]
9
+ def initialize(client: Container[:client])
10
+ @client = client
11
+ end
12
+
13
+ # @param credentials [Credentials]
14
+ # @param webhook [Hash]
15
+ #
16
+ # @return [Hash] response data
17
+ def call(credentials, webhook)
18
+ data = {**webhook, address: Shopify.config.webhook_uri}
19
+
20
+ @client.post_json(credentials, 'webhooks', webhook: data)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lucid/shopify'
4
+
5
+ module Lucid
6
+ module Shopify
7
+ class Credentials
8
+ extend Dry::Initializer
9
+
10
+ # @return [String]
11
+ param :myshopify_domain
12
+ # @return [String, nil] if {nil}, request will be unauthorised
13
+ param :access_token, optional: true
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lucid/shopify/container'
4
+
5
+ module Lucid
6
+ module Shopify
7
+ class DeleteAllWebhooks
8
+ # @param client [#get]
9
+ # @param delete_webhook [#call]
10
+ def initialize(client: Container[:client],
11
+ delete_webhook: Container[:delete_webhook])
12
+ @client = client
13
+ @delete_webhook = delete_webhook
14
+ end
15
+
16
+ # Delete any existing webhooks.
17
+ #
18
+ # @param credentials [Credentials]
19
+ #
20
+ # @return [Array<Hash>] response data
21
+ def call(credentials)
22
+ webhooks = @client.get(credentials, 'webhooks')['webhooks']
23
+
24
+ webhooks.map do |webhook|
25
+ Thread.new { @delete_webhook.(credentials, webhook['id']) }
26
+ end.map(&:value)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lucid/shopify'
4
+
5
+ module Lucid
6
+ module Shopify
7
+ class DeleteRequest < Request
8
+ # @private
9
+ #
10
+ # @param credentials [Credentials]
11
+ # @param path [String] the endpoint relative to the base URL
12
+ def initialize(credentials, path)
13
+ super(credentials, :delete, path)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lucid/shopify/container'
4
+
5
+ module Lucid
6
+ module Shopify
7
+ class DeleteWebhook
8
+ # @param client [#delete]
9
+ def initialize(client: Container[:client])
10
+ @client = client
11
+ end
12
+
13
+ # @param credentials [Credentials]
14
+ # @param id [Integer]
15
+ #
16
+ # @return [Hash] response data
17
+ def call(credentials, id)
18
+ @client.delete(credentials, "webhooks/#{id}")
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lucid
4
+ module Shopify
5
+ # Subclass this class for all gem exceptions, so that callers may rescue
6
+ # any subclass with:
7
+ #
8
+ # rescue Lucid::Shopify::Error => e
9
+ Error = Class.new(StandardError)
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lucid/shopify'
4
+
5
+ module Lucid
6
+ module Shopify
7
+ class GetRequest < Request
8
+ # @private
9
+ #
10
+ # @param credentials [Credentials]
11
+ # @param path [String] the endpoint relative to the base URL
12
+ # @param params [Hash] the query params
13
+ def initialize(credentials, path, params = {})
14
+ super(credentials, :get, path, params: params)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lucid/shopify'
4
+
5
+ module Lucid
6
+ module Shopify
7
+ class PostRequest < Request
8
+ # @private
9
+ #
10
+ # @param credentials [Credentials]
11
+ # @param path [String] the endpoint relative to the base URL
12
+ # @param json [Hash] the JSON request body
13
+ def initialize(credentials, path, json)
14
+ super(credentials, :post, path, json: json)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lucid/shopify'
4
+
5
+ module Lucid
6
+ module Shopify
7
+ class PutRequest < Request
8
+ # @private
9
+ #
10
+ # @param credentials [Credentials]
11
+ # @param path [String] the endpoint relative to the base URL
12
+ # @param json [Hash] the JSON request body
13
+ def initialize(credentials, path, json)
14
+ super(credentials, :put, path, json: json)
15
+ end
16
+ end
17
+ end
18
+ end