spree_square 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.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/.env +7 -0
  3. data/.gem_release.yml +3 -0
  4. data/.github/.dependabot.yml +11 -0
  5. data/.github/workflows/tests.yml +107 -0
  6. data/.gitignore +23 -0
  7. data/.rspec +3 -0
  8. data/.rubocop.yml +24 -0
  9. data/CHANGELOG.md +18 -0
  10. data/CONTRIBUTING.md +29 -0
  11. data/Gemfile +28 -0
  12. data/LICENSE.md +9 -0
  13. data/README.md +105 -0
  14. data/Rakefile +23 -0
  15. data/app/.gitkeep +0 -0
  16. data/app/assets/config/spree_square_manifest.js +5 -0
  17. data/app/assets/images/.keep +0 -0
  18. data/app/controllers/spree/admin/square_oauth_controller.rb +103 -0
  19. data/app/controllers/spree/admin/square_order_mappings_controller.rb +12 -0
  20. data/app/controllers/spree/admin/square_webhook_events_controller.rb +11 -0
  21. data/app/controllers/spree_square/webhooks_controller.rb +56 -0
  22. data/app/javascript/spree_square/application.js +16 -0
  23. data/app/javascript/spree_square/controllers/spree_square_controller.js +7 -0
  24. data/app/jobs/spree_square/base_job.rb +5 -0
  25. data/app/jobs/spree_square/catalog_webhook_job.rb +20 -0
  26. data/app/jobs/spree_square/inventory_webhook_job.rb +27 -0
  27. data/app/jobs/spree_square/order_push_job.rb +23 -0
  28. data/app/jobs/spree_square/order_webhook_job.rb +51 -0
  29. data/app/jobs/spree_square/reconciliation_job.rb +36 -0
  30. data/app/models/spree/line_item_decorator.rb +55 -0
  31. data/app/models/spree/variant_decorator.rb +19 -0
  32. data/app/models/spree_square/catalog_mapping.rb +25 -0
  33. data/app/models/spree_square/credential.rb +41 -0
  34. data/app/models/spree_square/line_item_modifier.rb +24 -0
  35. data/app/models/spree_square/location_mapping.rb +14 -0
  36. data/app/models/spree_square/modifier.rb +14 -0
  37. data/app/models/spree_square/modifier_list.rb +23 -0
  38. data/app/models/spree_square/order_mapping.rb +15 -0
  39. data/app/models/spree_square/product_modifier_list.rb +12 -0
  40. data/app/models/spree_square/taxon_mapping.rb +25 -0
  41. data/app/models/spree_square/webhook_event.rb +21 -0
  42. data/app/serializers/spree_square/line_item_serializer.rb +13 -0
  43. data/app/serializers/spree_square/product_serializer.rb +47 -0
  44. data/app/services/spree_square/alerting.rb +17 -0
  45. data/app/services/spree_square/cart/add_item.rb +53 -0
  46. data/app/services/spree_square/catalog_importer.rb +57 -0
  47. data/app/services/spree_square/catalog_object_mapper.rb +226 -0
  48. data/app/services/spree_square/client.rb +116 -0
  49. data/app/services/spree_square/find_line_item_by_variant.rb +21 -0
  50. data/app/services/spree_square/inventory_sync.rb +29 -0
  51. data/app/services/spree_square/oauth_client.rb +116 -0
  52. data/app/services/spree_square/order_builder.rb +79 -0
  53. data/app/services/spree_square/order_pusher.rb +68 -0
  54. data/app/services/spree_square/order_status_mapper.rb +87 -0
  55. data/app/services/spree_square/revalidator.rb +55 -0
  56. data/app/services/spree_square/webhook_verifier.rb +21 -0
  57. data/app/subscribers/spree_square/order_completed_subscriber.rb +15 -0
  58. data/app/views/spree/admin/square_oauth/show.html.erb +63 -0
  59. data/app/views/spree/admin/square_order_mappings/index.html.erb +5 -0
  60. data/app/views/spree/admin/square_webhook_events/index.html.erb +5 -0
  61. data/bin/importmap +9 -0
  62. data/bin/rails +8 -0
  63. data/config/importmap.rb +6 -0
  64. data/config/initializers/spree.rb +45 -0
  65. data/config/initializers/spree_admin_square_navigation.rb +25 -0
  66. data/config/initializers/spree_admin_square_tables.rb +86 -0
  67. data/config/locales/en.yml +5 -0
  68. data/config/routes.rb +25 -0
  69. data/db/migrate/20260809120001_create_spree_square_location_mappings.rb +12 -0
  70. data/db/migrate/20260809120002_create_spree_square_taxon_mappings.rb +16 -0
  71. data/db/migrate/20260809120003_create_spree_square_catalog_mappings.rb +20 -0
  72. data/db/migrate/20260809140001_create_spree_square_webhook_events.rb +26 -0
  73. data/db/migrate/20260809160001_create_spree_square_modifier_lists.rb +16 -0
  74. data/db/migrate/20260809160002_create_spree_square_modifiers.rb +15 -0
  75. data/db/migrate/20260809160003_create_spree_square_product_modifier_lists.rb +13 -0
  76. data/db/migrate/20260809160004_create_spree_square_line_item_modifiers.rb +16 -0
  77. data/db/migrate/20260809170001_create_spree_square_order_mappings.rb +17 -0
  78. data/db/migrate/20260810180001_create_spree_square_credentials.rb +28 -0
  79. data/lib/generators/spree_square/install/install_generator.rb +20 -0
  80. data/lib/spree_square/configuration.rb +13 -0
  81. data/lib/spree_square/engine.rb +65 -0
  82. data/lib/spree_square/factories.rb +16 -0
  83. data/lib/spree_square/version.rb +7 -0
  84. data/lib/spree_square.rb +12 -0
  85. data/lib/tasks/spree_square.rake +77 -0
  86. data/lib/tasks/spree_square_demo.rake +142 -0
  87. data/spree_square.gemspec +56 -0
  88. metadata +218 -0
@@ -0,0 +1,28 @@
1
+ class CreateSpreeSquareCredentials < ActiveRecord::Migration[8.1]
2
+ def change
3
+ create_table :spree_square_credentials do |t|
4
+ t.references :store, null: false, foreign_key: { to_table: :spree_stores }, index: { unique: true }
5
+
6
+ # Encrypted at the application layer (ActiveRecord::Encryption, see
7
+ # SpreeSquare::Credential) — stored as text since encrypted payloads are
8
+ # longer than the plaintext token.
9
+ t.text :access_token
10
+ t.text :refresh_token
11
+
12
+ # Square's own merchant identifier — how a webhook payload (which
13
+ # carries merchant_id, not a store id of ours) gets routed back to the
14
+ # right store.
15
+ t.string :square_merchant_id, null: false
16
+ t.string :square_environment, null: false, default: 'sandbox'
17
+ t.datetime :expires_at
18
+ t.datetime :refresh_token_expires_at
19
+ # `t.json`, not `t.jsonb`/`array: true` — the extension's own dummy
20
+ # app (spec/dummy) runs on SQLite, which supports neither.
21
+ t.json :scopes, default: [], null: false
22
+
23
+ t.timestamps
24
+ end
25
+
26
+ add_index :spree_square_credentials, :square_merchant_id, unique: true
27
+ end
28
+ end
@@ -0,0 +1,20 @@
1
+ module SpreeSquare
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ class_option :migrate, type: :boolean, default: true
5
+
6
+ def add_migrations
7
+ run 'bundle exec rake railties:install:migrations FROM=spree_square'
8
+ end
9
+
10
+ def run_migrations
11
+ run_migrations = options[:migrate] || ['', 'y', 'Y'].include?(ask('Would you like to run the migrations now? [Y/n]'))
12
+ if run_migrations
13
+ run 'bin/rails db:migrate'
14
+ else
15
+ puts 'Skipping rails db:migrate, don\'t forget to run it!'
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+ module SpreeSquare
2
+ class Configuration < Spree::Preferences::Configuration
3
+
4
+ # Some example preferences are shown below, for more information visit:
5
+ # https://docs.spreecommerce.org/developer/contributing/creating-an-extension
6
+
7
+ # preference :enabled, :boolean, default: true
8
+ # preference :dark_chocolate, :boolean, default: true
9
+ # preference :color, :string, default: 'Red'
10
+ # preference :favorite_number, :integer
11
+ # preference :supported_locales, :array, default: [:en]
12
+ end
13
+ end
@@ -0,0 +1,65 @@
1
+ module SpreeSquare
2
+ class Engine < Rails::Engine
3
+ require 'spree/core'
4
+ isolate_namespace Spree
5
+ engine_name 'spree_square'
6
+
7
+ config.generators do |g|
8
+ g.test_framework :rspec
9
+ end
10
+
11
+ initializer 'spree_square.environment', before: :load_config_initializers do |_app|
12
+ SpreeSquare::Config = SpreeSquare::Configuration.new
13
+ end
14
+
15
+ # Must run before Active Record's own "active_record_encryption.configuration"
16
+ # initializer reads config.active_record.encryption — a plain
17
+ # config/initializers/*.rb file is too late (that initializer already
18
+ # ran by the time :load_config_initializers fires) and fails silently
19
+ # into "Missing Active Record encryption credential" the first time
20
+ # SpreeSquare::Credential#access_token is touched. ENV, not
21
+ # config/credentials.yml.enc, for the same reason as everything else in
22
+ # this app: spree_host has no credentials.yml.enc, only .env. Living
23
+ # here (not spree_host) means the dummy test app gets it too, via its
24
+ # own spec/.env (see spec/spec_helper.rb's `dotenv/load`).
25
+ initializer 'spree_square.active_record_encryption', before: 'active_record_encryption.configuration' do |app|
26
+ next if ENV['ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY'].blank?
27
+
28
+ app.config.active_record.encryption.primary_key = ENV['ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY']
29
+ app.config.active_record.encryption.deterministic_key = ENV['ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY']
30
+ app.config.active_record.encryption.key_derivation_salt = ENV['ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT']
31
+ end
32
+
33
+ initializer 'spree_square.assets' do |app|
34
+ if app.config.respond_to?(:assets)
35
+ app.config.assets.paths << root.join('app/javascript')
36
+ app.config.assets.precompile += %w[spree_square_manifest]
37
+ end
38
+ end
39
+
40
+ initializer 'spree_square.importmap', before: 'importmap' do |app|
41
+ if app.config.respond_to?(:importmap)
42
+ app.config.importmap.paths << root.join('config/importmap.rb')
43
+ # https://github.com/rails/importmap-rails?tab=readme-ov-file#sweeping-the-cache-in-development-and-test
44
+ app.config.importmap.cache_sweepers << root.join('app/javascript')
45
+ end
46
+ end
47
+
48
+ # Decorator files (app/models/spree/*_decorator.rb) intentionally define
49
+ # a constant Zeitwerk maps to their path (e.g. Spree::LineItemDecorator)
50
+ # rather than something under the SpreeSquare:: namespace — but with
51
+ # eager_load off in development, nothing ever references that exact
52
+ # constant name, so lazy autoloading never triggers it and the
53
+ # `SomeClass.prepend(...)` line at the bottom of the file never runs.
54
+ # spree_core's own engine force-loads *its* decorators the same way
55
+ # (lib/spree/core/engine.rb) — each engine has to do this for its own
56
+ # app/ tree; there's no shared mechanism that covers extensions too.
57
+ def self.activate
58
+ Dir.glob(File.join(File.dirname(__FILE__), '../../app/**/*_decorator*.rb')) do |c|
59
+ Rails.application.config.cache_classes ? require(c) : load(c)
60
+ end
61
+ end
62
+
63
+ config.to_prepare(&method(:activate).to_proc)
64
+ end
65
+ end
@@ -0,0 +1,16 @@
1
+ FactoryBot.define do
2
+ # Define your Spree extensions Factories within this file to enable applications, and other extensions to use and override them.
3
+ #
4
+ # Example adding this to your spec_helper will load these Factories for use:
5
+ # require 'spree_square/factories'
6
+
7
+ factory :square_credential, class: 'SpreeSquare::Credential' do
8
+ store
9
+ sequence(:square_merchant_id) { |i| "MERCHANT#{i}" }
10
+ access_token { 'test-access-token' }
11
+ refresh_token { 'test-refresh-token' }
12
+ square_environment { 'sandbox' }
13
+ expires_at { 30.days.from_now }
14
+ refresh_token_expires_at { 90.days.from_now }
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ module SpreeSquare
2
+ VERSION = '0.1.0'.freeze
3
+
4
+ def gem_version
5
+ Gem::Version.new(VERSION)
6
+ end
7
+ end
@@ -0,0 +1,12 @@
1
+ require 'spree'
2
+ require 'spree_square/engine'
3
+ require 'spree_square/version'
4
+ require 'spree_square/configuration'
5
+
6
+ module SpreeSquare
7
+ mattr_accessor :queue
8
+
9
+ def self.queue
10
+ @@queue ||= Spree.queues.default
11
+ end
12
+ end
@@ -0,0 +1,77 @@
1
+ namespace :spree_square do
2
+ desc 'M1 verification: list catalog objects from the configured Square sandbox/production account'
3
+ task list_catalog: :environment do
4
+ client = SpreeSquare::Client.instance
5
+
6
+ puts "Environment: #{ENV.fetch('SQUARE_ENVIRONMENT', 'sandbox')}"
7
+ puts "Location ID: #{client.location_id}"
8
+ puts '---'
9
+
10
+ count = 0
11
+ client.catalog.list(types: 'ITEM,CATEGORY,IMAGE,MODIFIER_LIST,MODIFIER').each do |object|
12
+ count += 1
13
+ # Fern generates one CatalogObject subclass per `type`, and each only
14
+ # defines the data accessor for its own type (e.g.
15
+ # Square::Types::CatalogObjectModifierList has no `item_data` method
16
+ # at all) — `&.` doesn't help against a NoMethodError, so check
17
+ # `respond_to?` first.
18
+ name = %i[item_data category_data modifier_list_data modifier_data image_data]
19
+ .filter_map { |accessor| object.public_send(accessor)&.name if object.respond_to?(accessor) }
20
+ .first || '(unnamed)'
21
+ puts "#{object.type.ljust(14)} #{object.id.ljust(24)} #{name}"
22
+ end
23
+
24
+ puts '---'
25
+ puts count.zero? ? 'No catalog objects found — this sandbox account has an empty catalog (expected for a fresh account).' : "#{count} object(s) found."
26
+ end
27
+
28
+ desc 'M2: import the full Square catalog (items + categories) into Spree'
29
+ task import_catalog: :environment do
30
+ result = SpreeSquare::CatalogImporter.call
31
+ category_word = result.categories_count == 1 ? 'category' : 'categories'
32
+ puts "Imported #{result.categories_count} #{category_word} and #{result.items_count} item(s)."
33
+ puts '---'
34
+ SpreeSquare::CatalogMapping.where(square_object_type: SpreeSquare::CatalogMapping::ITEM)
35
+ .includes(:product).find_each do |mapping|
36
+ product = mapping.product
37
+ next unless product
38
+
39
+ puts "#{product.slug.ljust(40)} #{product.display_price}"
40
+ end
41
+ end
42
+
43
+ # Usage: bin/rails "spree_square:setup_webhook[https://<id>.ngrok-free.app/spree_square/webhooks/square]"
44
+ desc 'M3: create or update the Square webhook subscription for a notification URL'
45
+ task :setup_webhook, [:url] => :environment do |_, args|
46
+ url = args[:url]
47
+ abort 'Usage: bin/rails "spree_square:setup_webhook[https://your-tunnel/spree_square/webhooks/square]"' if url.blank?
48
+
49
+ client = SpreeSquare::Client.instance
50
+ event_types = %w[catalog.version.updated inventory.count.updated order.updated order.fulfillment.updated]
51
+ existing = client.webhooks.subscriptions.list.find { |s| s.name == 'spree_square' }
52
+
53
+ if existing
54
+ client.webhooks.subscriptions.update(
55
+ subscription_id: existing.id,
56
+ subscription: { notification_url: url, event_types: event_types, enabled: true }
57
+ )
58
+ # `update` doesn't return the signature key (Square only returns it on
59
+ # create/rotate) — fetch a fresh one explicitly so we always have a
60
+ # usable value to put in .env.
61
+ key_response = client.webhooks.subscriptions.update_signature_key(subscription_id: existing.id)
62
+ puts "Updated webhook subscription #{existing.id}"
63
+ puts "Signature key: #{key_response.signature_key}"
64
+ else
65
+ response = client.webhooks.subscriptions.create(
66
+ idempotency_key: SecureRandom.uuid,
67
+ subscription: { name: 'spree_square', notification_url: url, event_types: event_types }
68
+ )
69
+ subscription = response.subscription
70
+ puts "Created webhook subscription #{subscription.id}"
71
+ puts "Signature key: #{subscription.signature_key}"
72
+ end
73
+
74
+ puts '---'
75
+ puts 'Set SQUARE_WEBHOOK_SIGNATURE_KEY in .env to the value above, then recreate the web container.'
76
+ end
77
+ end
@@ -0,0 +1,142 @@
1
+ # Demo/dev convenience, not shipped in the packaged gem (see spree_square.gemspec's
2
+ # `s.files` — this file and demo_menu*/demo_menu_images/ are excluded from `s.files`
3
+ # on purpose: they're restaurant-menu-specific example content, not something every
4
+ # installer of this extension wants bundled in). Kept in the source repo as a
5
+ # reference for how to seed a Square sandbox catalog end to end.
6
+ #
7
+ # square.rb doesn't require this from its main entrypoint (it's an opt-in
8
+ # multipart-upload helper, only needed by tasks that upload files — just this
9
+ # one), so it isn't loaded by `require 'square'` alone.
10
+ require 'square/file_param'
11
+
12
+ namespace :spree_square do
13
+ # Demo/dev convenience: populates the configured Square sandbox with a full
14
+ # restaurant menu (categories, items, prices, photos) so there's something
15
+ # real to look at besides the starter's single sample item. Square stays
16
+ # the source of truth even for demo data — this pushes INTO Square, then
17
+ # runs the normal M2 importer to pull it down into Spree, exactly like a
18
+ # real menu edit would flow.
19
+ #
20
+ # Usage:
21
+ # bin/rails spree_square:seed_demo_menu
22
+ # CLEAR_CATALOG=true bin/rails spree_square:seed_demo_menu # wipe the sandbox catalog first
23
+ #
24
+ # Re-running without CLEAR_CATALOG=true creates duplicate items — Square's
25
+ # batch-upsert only updates in place when given a real (non-temporary)
26
+ # object id, and this task always creates fresh ones. Fine for a sandbox;
27
+ # clear first if you want a clean slate.
28
+ desc 'Demo: seed the Square sandbox catalog with a full sample restaurant menu (with photos)'
29
+ task seed_demo_menu: :environment do
30
+ client = SpreeSquare::Client.instance
31
+ menu_path = File.join(__dir__, 'demo_menu.json')
32
+ images_dir = File.join(__dir__, 'demo_menu_images')
33
+ menu = JSON.parse(File.read(menu_path))
34
+
35
+ if ActiveModel::Type::Boolean.new.cast(ENV['CLEAR_CATALOG'])
36
+ existing_ids = client.catalog.list(types: 'ITEM,CATEGORY,IMAGE,MODIFIER_LIST,MODIFIER').map(&:id)
37
+ if existing_ids.any?
38
+ existing_ids.each_slice(200) { |slice| client.catalog.batch_delete(object_ids: slice) }
39
+ puts "Cleared #{existing_ids.size} existing catalog object(s)."
40
+ # BatchDeleteCatalogObjects is immediately consistent for direct
41
+ # reads, but the SearchCatalogObjects index (what both this task's
42
+ # own duplicate-check below and CatalogImporter use) lags behind by
43
+ # a few seconds — without this pause, the import at the end of this
44
+ # task can still see the just-deleted objects and create duplicate
45
+ # Spree categories/products before Square's index catches up.
46
+ sleep 5
47
+ end
48
+ end
49
+
50
+ objects = []
51
+ menu.each_key do |category_name|
52
+ objects << {
53
+ type: 'CATEGORY',
54
+ id: "#cat-#{category_name.parameterize}",
55
+ category_data: { name: category_name }
56
+ }
57
+ end
58
+
59
+ menu.each do |category_name, items|
60
+ items.each do |item|
61
+ item_temp_id = "#item-#{item['image_slug']}"
62
+ objects << {
63
+ type: 'ITEM',
64
+ id: item_temp_id,
65
+ item_data: {
66
+ name: item['name'],
67
+ description: item['description'],
68
+ # `category_id` (singular) is a legacy field this Square API
69
+ # version silently drops on write — verified by round-tripping
70
+ # a test item through upsert and reading it back via search:
71
+ # category_id came back nil every time, `categories` (array)
72
+ # is what actually persists. CatalogObjectMapper#assign_taxons!
73
+ # already reads both, so no mapper change needed.
74
+ categories: [{ id: "#cat-#{category_name.parameterize}" }],
75
+ variations: [
76
+ {
77
+ type: 'ITEM_VARIATION',
78
+ id: "#{item_temp_id}-regular",
79
+ item_variation_data: {
80
+ name: 'Regular',
81
+ pricing_type: 'FIXED_PRICING',
82
+ price_money: { amount: item['price_cents'], currency: 'USD' }
83
+ }
84
+ }
85
+ ]
86
+ }
87
+ }
88
+ end
89
+ end
90
+
91
+ puts "Pushing #{menu.values.sum(&:size)} item(s) across #{menu.size} categories to Square..."
92
+ response = client.catalog.batch_upsert(
93
+ idempotency_key: SecureRandom.uuid,
94
+ batches: [{ objects: objects }]
95
+ )
96
+ # `object_id_` (trailing underscore), not `object_id` — the SDK avoids
97
+ # shadowing Ruby's own Object#object_id; `api_name: "object_id"` is just
98
+ # the JSON wire name.
99
+ id_by_temp_id = response.id_mappings.to_h { |m| [m.client_object_id, m.object_id_] }
100
+
101
+ puts 'Uploading photos...'
102
+ uploaded = 0
103
+ menu.each_value do |items|
104
+ items.each do |item|
105
+ item_temp_id = "#item-#{item['image_slug']}"
106
+ real_item_id = id_by_temp_id[item_temp_id]
107
+ image_path = File.join(images_dir, "#{item['image_slug']}.jpg")
108
+ unless real_item_id && File.exist?(image_path)
109
+ puts " skipped #{item['name']} (no #{real_item_id ? 'image file' : 'mapped id'})"
110
+ next
111
+ end
112
+
113
+ # `images.create`'s multipart builder writes `request:` verbatim
114
+ # (Square::Internal::Multipart::FormData#add has no Hash case, only
115
+ # String/Integer/Float/Boolean/#read) — an unserialized Hash gets
116
+ # written as Ruby's `Hash#to_s`, not JSON, so the request must be
117
+ # JSON-encoded ourselves first.
118
+ client.catalog.images.create(
119
+ request: {
120
+ idempotency_key: SecureRandom.uuid,
121
+ object_id: real_item_id,
122
+ image: { type: 'IMAGE', id: "#image-#{item['image_slug']}", image_data: { name: "#{item['name']} photo" } },
123
+ is_primary: true
124
+ }.to_json,
125
+ image_file: Square::FileParam.from_filepath(filepath: image_path, content_type: 'image/jpeg')
126
+ )
127
+ uploaded += 1
128
+ end
129
+ end
130
+ puts "Uploaded #{uploaded} photo(s)."
131
+
132
+ puts '---'
133
+ puts 'Importing into Spree...'
134
+ # Square's SearchCatalogObjects index lagged the writes above by well
135
+ # over 10s in practice during development of this task — a short pause
136
+ # isn't enough to trust the search results that CatalogImporter relies on.
137
+ sleep 20
138
+ result = SpreeSquare::CatalogImporter.call
139
+ puts "Imported #{result.categories_count} categories and #{result.items_count} item(s) into Spree."
140
+ puts 'If any items are missing, they just need the index to catch up — re-run: bin/rails spree_square:import_catalog'
141
+ end
142
+ end
@@ -0,0 +1,56 @@
1
+ # encoding: UTF-8
2
+ lib = File.expand_path('../lib/', __FILE__)
3
+ $LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
4
+
5
+ require 'spree_square/version'
6
+
7
+ Gem::Specification.new do |s|
8
+ s.platform = Gem::Platform::RUBY
9
+ s.name = 'spree_square'
10
+ s.version = SpreeSquare::VERSION
11
+ s.summary = 'Spree Commerce Square Extension'
12
+ s.description = 'Syncs a Square catalog (items, variations, categories, images, modifiers) into ' \
13
+ 'Spree Commerce, pushes completed Spree orders into Square as paid tickets, and ' \
14
+ 'keeps fulfillment status in sync in both directions via webhooks. Supports ' \
15
+ 'self-service OAuth ("Connect to Square" in the admin) as well as a hand-issued ' \
16
+ 'access token for local development.'
17
+ s.required_ruby_version = '>= 3.2'
18
+
19
+ s.author = 'Amit Solanki'
20
+ s.email = 'amitkssolanki@gmail.com'
21
+ s.homepage = 'https://github.com/amitkssolanki/spree_square'
22
+ s.license = 'MIT'
23
+
24
+ s.metadata = {
25
+ 'homepage_uri' => s.homepage,
26
+ 'source_code_uri' => s.homepage,
27
+ 'changelog_uri' => "#{s.homepage}/blob/main/CHANGELOG.md",
28
+ 'bug_tracker_uri' => "#{s.homepage}/issues"
29
+ }
30
+
31
+ # `git ls-files` (not a manual Dir[] glob) so the package always matches
32
+ # what's actually tracked in the repo — and so the demo-menu example
33
+ # content below (restaurant-specific, not something every installer wants
34
+ # bundled in) can be excluded by path without needing to keep two lists in
35
+ # sync by hand.
36
+ s.files = `git ls-files -z`.split("\x0").reject do |f|
37
+ (f.start_with?('spec/') && !f.start_with?('spec/fixtures')) ||
38
+ f.start_with?('lib/tasks/demo_menu')
39
+ end
40
+ s.require_path = 'lib'
41
+ s.requirements << 'none'
42
+
43
+ spree_version = '>= 5.4.0.beta'
44
+ s.add_dependency 'spree', spree_version
45
+ s.add_dependency 'spree_admin', spree_version
46
+
47
+ # Square's official Ruby SDK. Current major (Fern-generated) uses
48
+ # Square::Client.new(token:) / square.orders.create(...) — NOT the legacy
49
+ # client.orders_api.create_order shape most tutorials show (that's the
50
+ # square_legacy export within this same gem; we don't use it).
51
+ s.add_dependency 'square.rb', '~> 46.0'
52
+
53
+ s.add_development_dependency 'spree_dev_tools'
54
+ s.add_development_dependency 'webmock'
55
+ s.add_development_dependency 'gem-release'
56
+ end
metadata ADDED
@@ -0,0 +1,218 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: spree_square
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Amit Solanki
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: spree
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 5.4.0.beta
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 5.4.0.beta
26
+ - !ruby/object:Gem::Dependency
27
+ name: spree_admin
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 5.4.0.beta
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 5.4.0.beta
40
+ - !ruby/object:Gem::Dependency
41
+ name: square.rb
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '46.0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '46.0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: spree_dev_tools
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: webmock
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ type: :development
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ - !ruby/object:Gem::Dependency
83
+ name: gem-release
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: '0'
89
+ type: :development
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ description: Syncs a Square catalog (items, variations, categories, images, modifiers)
97
+ into Spree Commerce, pushes completed Spree orders into Square as paid tickets,
98
+ and keeps fulfillment status in sync in both directions via webhooks. Supports self-service
99
+ OAuth ("Connect to Square" in the admin) as well as a hand-issued access token for
100
+ local development.
101
+ email: amitkssolanki@gmail.com
102
+ executables: []
103
+ extensions: []
104
+ extra_rdoc_files: []
105
+ files:
106
+ - ".env"
107
+ - ".gem_release.yml"
108
+ - ".github/.dependabot.yml"
109
+ - ".github/workflows/tests.yml"
110
+ - ".gitignore"
111
+ - ".rspec"
112
+ - ".rubocop.yml"
113
+ - CHANGELOG.md
114
+ - CONTRIBUTING.md
115
+ - Gemfile
116
+ - LICENSE.md
117
+ - README.md
118
+ - Rakefile
119
+ - app/.gitkeep
120
+ - app/assets/config/spree_square_manifest.js
121
+ - app/assets/images/.keep
122
+ - app/controllers/spree/admin/square_oauth_controller.rb
123
+ - app/controllers/spree/admin/square_order_mappings_controller.rb
124
+ - app/controllers/spree/admin/square_webhook_events_controller.rb
125
+ - app/controllers/spree_square/webhooks_controller.rb
126
+ - app/javascript/spree_square/application.js
127
+ - app/javascript/spree_square/controllers/spree_square_controller.js
128
+ - app/jobs/spree_square/base_job.rb
129
+ - app/jobs/spree_square/catalog_webhook_job.rb
130
+ - app/jobs/spree_square/inventory_webhook_job.rb
131
+ - app/jobs/spree_square/order_push_job.rb
132
+ - app/jobs/spree_square/order_webhook_job.rb
133
+ - app/jobs/spree_square/reconciliation_job.rb
134
+ - app/models/spree/line_item_decorator.rb
135
+ - app/models/spree/variant_decorator.rb
136
+ - app/models/spree_square/catalog_mapping.rb
137
+ - app/models/spree_square/credential.rb
138
+ - app/models/spree_square/line_item_modifier.rb
139
+ - app/models/spree_square/location_mapping.rb
140
+ - app/models/spree_square/modifier.rb
141
+ - app/models/spree_square/modifier_list.rb
142
+ - app/models/spree_square/order_mapping.rb
143
+ - app/models/spree_square/product_modifier_list.rb
144
+ - app/models/spree_square/taxon_mapping.rb
145
+ - app/models/spree_square/webhook_event.rb
146
+ - app/serializers/spree_square/line_item_serializer.rb
147
+ - app/serializers/spree_square/product_serializer.rb
148
+ - app/services/spree_square/alerting.rb
149
+ - app/services/spree_square/cart/add_item.rb
150
+ - app/services/spree_square/catalog_importer.rb
151
+ - app/services/spree_square/catalog_object_mapper.rb
152
+ - app/services/spree_square/client.rb
153
+ - app/services/spree_square/find_line_item_by_variant.rb
154
+ - app/services/spree_square/inventory_sync.rb
155
+ - app/services/spree_square/oauth_client.rb
156
+ - app/services/spree_square/order_builder.rb
157
+ - app/services/spree_square/order_pusher.rb
158
+ - app/services/spree_square/order_status_mapper.rb
159
+ - app/services/spree_square/revalidator.rb
160
+ - app/services/spree_square/webhook_verifier.rb
161
+ - app/subscribers/spree_square/order_completed_subscriber.rb
162
+ - app/views/spree/admin/square_oauth/show.html.erb
163
+ - app/views/spree/admin/square_order_mappings/index.html.erb
164
+ - app/views/spree/admin/square_webhook_events/index.html.erb
165
+ - bin/importmap
166
+ - bin/rails
167
+ - config/importmap.rb
168
+ - config/initializers/spree.rb
169
+ - config/initializers/spree_admin_square_navigation.rb
170
+ - config/initializers/spree_admin_square_tables.rb
171
+ - config/locales/en.yml
172
+ - config/routes.rb
173
+ - db/migrate/20260809120001_create_spree_square_location_mappings.rb
174
+ - db/migrate/20260809120002_create_spree_square_taxon_mappings.rb
175
+ - db/migrate/20260809120003_create_spree_square_catalog_mappings.rb
176
+ - db/migrate/20260809140001_create_spree_square_webhook_events.rb
177
+ - db/migrate/20260809160001_create_spree_square_modifier_lists.rb
178
+ - db/migrate/20260809160002_create_spree_square_modifiers.rb
179
+ - db/migrate/20260809160003_create_spree_square_product_modifier_lists.rb
180
+ - db/migrate/20260809160004_create_spree_square_line_item_modifiers.rb
181
+ - db/migrate/20260809170001_create_spree_square_order_mappings.rb
182
+ - db/migrate/20260810180001_create_spree_square_credentials.rb
183
+ - lib/generators/spree_square/install/install_generator.rb
184
+ - lib/spree_square.rb
185
+ - lib/spree_square/configuration.rb
186
+ - lib/spree_square/engine.rb
187
+ - lib/spree_square/factories.rb
188
+ - lib/spree_square/version.rb
189
+ - lib/tasks/spree_square.rake
190
+ - lib/tasks/spree_square_demo.rake
191
+ - spree_square.gemspec
192
+ homepage: https://github.com/amitkssolanki/spree_square
193
+ licenses:
194
+ - MIT
195
+ metadata:
196
+ homepage_uri: https://github.com/amitkssolanki/spree_square
197
+ source_code_uri: https://github.com/amitkssolanki/spree_square
198
+ changelog_uri: https://github.com/amitkssolanki/spree_square/blob/main/CHANGELOG.md
199
+ bug_tracker_uri: https://github.com/amitkssolanki/spree_square/issues
200
+ rdoc_options: []
201
+ require_paths:
202
+ - lib
203
+ required_ruby_version: !ruby/object:Gem::Requirement
204
+ requirements:
205
+ - - ">="
206
+ - !ruby/object:Gem::Version
207
+ version: '3.2'
208
+ required_rubygems_version: !ruby/object:Gem::Requirement
209
+ requirements:
210
+ - - ">="
211
+ - !ruby/object:Gem::Version
212
+ version: '0'
213
+ requirements:
214
+ - none
215
+ rubygems_version: 3.6.9
216
+ specification_version: 4
217
+ summary: Spree Commerce Square Extension
218
+ test_files: []