heyquarry-shopify-profile-sync 0.1.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: d468da3b0e40914a555e63fe18f42324843da1cf9a34efc14bcce700b4356e7b
4
+ data.tar.gz: 4241e02242faf3241fc5c574fd2c3aa87d69ce5ceec628ef28ae34d26df5766f
5
+ SHA512:
6
+ metadata.gz: 3f02c4cfad47cfcca693269c4f103a262632ea20fa31d74d9cd2db8c26971fbd152c3a255e2d1a2d55d52612aab3e3ba7e5e5f5e63aa5dbfb50c083ac8fa03f1
7
+ data.tar.gz: a70874641192926d0028392f09fa167a07071a11c5cc3be01bb79e74d13b8ba2273d140e701815f4533fb00c422f165b97226b02a2aac6531d44f004333c505c
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HeyQuarry
4
+ module ShopifyProfileSync
5
+ SHOP_PROFILE_QUERY = <<~GRAPHQL.squish
6
+ query HeyQuarryShopProfile {
7
+ shop {
8
+ id
9
+ name
10
+ email
11
+ contactEmail
12
+ myshopifyDomain
13
+ description
14
+ plan { displayName }
15
+ billingAddress { phone countryCodeV2 }
16
+ primaryDomain { host }
17
+ }
18
+ }
19
+ GRAPHQL
20
+
21
+ module ShopQuery
22
+ module_function
23
+
24
+ def map_shop_to_payload(shop)
25
+ return nil unless shop.is_a?(Hash) && shop["myshopifyDomain"]
26
+
27
+ email = shop["email"]&.strip
28
+ email = shop["contactEmail"]&.strip if email.nil? || email.empty?
29
+
30
+ payload = { shopDomain: shop["myshopifyDomain"] }
31
+ payload[:email] = email if email && !email.empty?
32
+ payload[:phone] = shop.dig("billingAddress", "phone") if shop.dig("billingAddress", "phone")
33
+ payload[:shopifyPlan] = shop.dig("plan", "displayName") if shop.dig("plan", "displayName")
34
+ payload[:countryCode] = shop.dig("billingAddress", "countryCodeV2") if shop.dig("billingAddress", "countryCodeV2")
35
+ payload[:customDomain] = shop.dig("primaryDomain", "host") if shop.dig("primaryDomain", "host")
36
+ payload[:about] = shop["description"] if shop["description"]
37
+ payload[:shopGid] = shop["id"] if shop["id"]
38
+ payload[:shopName] = shop["name"] if shop["name"]
39
+ payload
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module HeyQuarry
8
+ module ShopifyProfileSync
9
+ DEFAULT_PROFILE_URL = "https://www.heyquarry.com/api/shopify/shops/profile"
10
+ DEFAULT_ADMIN_API_VERSION = "2026-01"
11
+
12
+ SyncResult = Struct.new(:ok, :skipped, :shop_domain, :status, :error, keyword_init: true)
13
+
14
+ module Sync
15
+ module_function
16
+
17
+ def sync_from_session(shop_domain:, access_token:, api_key: nil, profile_url: nil, admin_api_version: DEFAULT_ADMIN_API_VERSION)
18
+ resolved = resolve_options(api_key: api_key, profile_url: profile_url)
19
+ shop = fetch_shop_from_session(shop_domain, access_token, admin_api_version)
20
+ payload = ShopQuery.map_shop_to_payload(shop)
21
+ return skipped("Shop profile missing myshopifyDomain") unless payload
22
+
23
+ post_profile(resolved[:profile_url], resolved[:api_key], payload)
24
+ end
25
+
26
+ def sync_from_admin_client(client, api_key: nil, profile_url: nil)
27
+ resolved = resolve_options(api_key: api_key, profile_url: profile_url)
28
+ response = client.query(query: SHOP_PROFILE_QUERY)
29
+ shop = response.body.dig("data", "shop")
30
+ payload = ShopQuery.map_shop_to_payload(shop)
31
+ return skipped("Shop profile missing myshopifyDomain") unless payload
32
+
33
+ post_profile(resolved[:profile_url], resolved[:api_key], payload)
34
+ end
35
+
36
+ def post_profile_to_heyquarry(payload, api_key: nil, profile_url: nil)
37
+ resolved = resolve_options(api_key: api_key, profile_url: profile_url)
38
+ return skipped("shopDomain is required") unless payload.is_a?(Hash) && payload[:shopDomain]
39
+
40
+ post_profile(resolved[:profile_url], resolved[:api_key], payload)
41
+ end
42
+
43
+ def resolve_options(api_key:, profile_url:)
44
+ key = api_key || ENV["HEYQUARRY_APP_API_KEY"]
45
+ raise ArgumentError, "HeyQuarry app API key is required. Pass api_key: or set HEYQUARRY_APP_API_KEY." if key.nil? || key.strip.empty?
46
+
47
+ url = profile_url || ENV["HEYQUARRY_PROFILE_URL"] || DEFAULT_PROFILE_URL
48
+ { api_key: key.strip, profile_url: url.sub(%r{/\z}, "") }
49
+ end
50
+
51
+ def fetch_shop_from_session(shop_domain, access_token, admin_api_version)
52
+ host = shop_domain.sub(%r{\Ahttps?://}, "").sub(%r{/\z}, "")
53
+ uri = URI("https://#{host}/admin/api/#{admin_api_version}/graphql.json")
54
+ request = Net::HTTP::Post.new(uri)
55
+ request["Content-Type"] = "application/json"
56
+ request["X-Shopify-Access-Token"] = access_token
57
+ request.body = JSON.generate(query: SHOP_PROFILE_QUERY)
58
+
59
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
60
+ JSON.parse(response.body).dig("data", "shop")
61
+ end
62
+
63
+ def post_profile(profile_url, api_key, payload)
64
+ uri = URI(profile_url)
65
+ request = Net::HTTP::Post.new(uri)
66
+ request["Content-Type"] = "application/json"
67
+ request["X-App-Api-Key"] = api_key
68
+ request.body = JSON.generate(payload.transform_keys(&:to_s))
69
+
70
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(request) }
71
+ if response.is_a?(Net::HTTPSuccess)
72
+ SyncResult.new(ok: true, shop_domain: payload[:shopDomain], status: response.code.to_i)
73
+ else
74
+ SyncResult.new(
75
+ ok: false,
76
+ shop_domain: payload[:shopDomain],
77
+ status: response.code.to_i,
78
+ error: response.body.to_s[0, 300].presence || response.message,
79
+ )
80
+ end
81
+ end
82
+
83
+ def skipped(message)
84
+ SyncResult.new(ok: false, skipped: true, error: message)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HeyQuarry
4
+ module ShopifyProfileSync
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "shopify_profile_sync/version"
4
+ require_relative "shopify_profile_sync/shop_query"
5
+ require_relative "shopify_profile_sync/sync"
6
+
7
+ module HeyQuarry
8
+ module ShopifyProfileSync
9
+ end
10
+ end
metadata ADDED
@@ -0,0 +1,47 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: heyquarry-shopify-profile-sync
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - HeyQuarry
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-06-25 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email:
15
+ - support@heyquarry.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/heyquarry/shopify_profile_sync.rb
21
+ - lib/heyquarry/shopify_profile_sync/shop_query.rb
22
+ - lib/heyquarry/shopify_profile_sync/sync.rb
23
+ - lib/heyquarry/shopify_profile_sync/version.rb
24
+ homepage: https://heyquarry.com
25
+ licenses:
26
+ - MIT
27
+ metadata: {}
28
+ post_install_message:
29
+ rdoc_options: []
30
+ require_paths:
31
+ - lib
32
+ required_ruby_version: !ruby/object:Gem::Requirement
33
+ requirements:
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 3.0.0
37
+ required_rubygems_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '0'
42
+ requirements: []
43
+ rubygems_version: 3.5.22
44
+ signing_key:
45
+ specification_version: 4
46
+ summary: Sync Shopify shop profile (email, plan, phone) to HeyQuarry after OAuth install.
47
+ test_files: []