lucid-shopify 0.34.0

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