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.
- checksums.yaml +7 -0
- data/.env +7 -0
- data/.gem_release.yml +3 -0
- data/.github/.dependabot.yml +11 -0
- data/.github/workflows/tests.yml +107 -0
- data/.gitignore +23 -0
- data/.rspec +3 -0
- data/.rubocop.yml +24 -0
- data/CHANGELOG.md +18 -0
- data/CONTRIBUTING.md +29 -0
- data/Gemfile +28 -0
- data/LICENSE.md +9 -0
- data/README.md +105 -0
- data/Rakefile +23 -0
- data/app/.gitkeep +0 -0
- data/app/assets/config/spree_square_manifest.js +5 -0
- data/app/assets/images/.keep +0 -0
- data/app/controllers/spree/admin/square_oauth_controller.rb +103 -0
- data/app/controllers/spree/admin/square_order_mappings_controller.rb +12 -0
- data/app/controllers/spree/admin/square_webhook_events_controller.rb +11 -0
- data/app/controllers/spree_square/webhooks_controller.rb +56 -0
- data/app/javascript/spree_square/application.js +16 -0
- data/app/javascript/spree_square/controllers/spree_square_controller.js +7 -0
- data/app/jobs/spree_square/base_job.rb +5 -0
- data/app/jobs/spree_square/catalog_webhook_job.rb +20 -0
- data/app/jobs/spree_square/inventory_webhook_job.rb +27 -0
- data/app/jobs/spree_square/order_push_job.rb +23 -0
- data/app/jobs/spree_square/order_webhook_job.rb +51 -0
- data/app/jobs/spree_square/reconciliation_job.rb +36 -0
- data/app/models/spree/line_item_decorator.rb +55 -0
- data/app/models/spree/variant_decorator.rb +19 -0
- data/app/models/spree_square/catalog_mapping.rb +25 -0
- data/app/models/spree_square/credential.rb +41 -0
- data/app/models/spree_square/line_item_modifier.rb +24 -0
- data/app/models/spree_square/location_mapping.rb +14 -0
- data/app/models/spree_square/modifier.rb +14 -0
- data/app/models/spree_square/modifier_list.rb +23 -0
- data/app/models/spree_square/order_mapping.rb +15 -0
- data/app/models/spree_square/product_modifier_list.rb +12 -0
- data/app/models/spree_square/taxon_mapping.rb +25 -0
- data/app/models/spree_square/webhook_event.rb +21 -0
- data/app/serializers/spree_square/line_item_serializer.rb +13 -0
- data/app/serializers/spree_square/product_serializer.rb +47 -0
- data/app/services/spree_square/alerting.rb +17 -0
- data/app/services/spree_square/cart/add_item.rb +53 -0
- data/app/services/spree_square/catalog_importer.rb +57 -0
- data/app/services/spree_square/catalog_object_mapper.rb +226 -0
- data/app/services/spree_square/client.rb +116 -0
- data/app/services/spree_square/find_line_item_by_variant.rb +21 -0
- data/app/services/spree_square/inventory_sync.rb +29 -0
- data/app/services/spree_square/oauth_client.rb +116 -0
- data/app/services/spree_square/order_builder.rb +79 -0
- data/app/services/spree_square/order_pusher.rb +68 -0
- data/app/services/spree_square/order_status_mapper.rb +87 -0
- data/app/services/spree_square/revalidator.rb +55 -0
- data/app/services/spree_square/webhook_verifier.rb +21 -0
- data/app/subscribers/spree_square/order_completed_subscriber.rb +15 -0
- data/app/views/spree/admin/square_oauth/show.html.erb +63 -0
- data/app/views/spree/admin/square_order_mappings/index.html.erb +5 -0
- data/app/views/spree/admin/square_webhook_events/index.html.erb +5 -0
- data/bin/importmap +9 -0
- data/bin/rails +8 -0
- data/config/importmap.rb +6 -0
- data/config/initializers/spree.rb +45 -0
- data/config/initializers/spree_admin_square_navigation.rb +25 -0
- data/config/initializers/spree_admin_square_tables.rb +86 -0
- data/config/locales/en.yml +5 -0
- data/config/routes.rb +25 -0
- data/db/migrate/20260809120001_create_spree_square_location_mappings.rb +12 -0
- data/db/migrate/20260809120002_create_spree_square_taxon_mappings.rb +16 -0
- data/db/migrate/20260809120003_create_spree_square_catalog_mappings.rb +20 -0
- data/db/migrate/20260809140001_create_spree_square_webhook_events.rb +26 -0
- data/db/migrate/20260809160001_create_spree_square_modifier_lists.rb +16 -0
- data/db/migrate/20260809160002_create_spree_square_modifiers.rb +15 -0
- data/db/migrate/20260809160003_create_spree_square_product_modifier_lists.rb +13 -0
- data/db/migrate/20260809160004_create_spree_square_line_item_modifiers.rb +16 -0
- data/db/migrate/20260809170001_create_spree_square_order_mappings.rb +17 -0
- data/db/migrate/20260810180001_create_spree_square_credentials.rb +28 -0
- data/lib/generators/spree_square/install/install_generator.rb +20 -0
- data/lib/spree_square/configuration.rb +13 -0
- data/lib/spree_square/engine.rb +65 -0
- data/lib/spree_square/factories.rb +16 -0
- data/lib/spree_square/version.rb +7 -0
- data/lib/spree_square.rb +12 -0
- data/lib/tasks/spree_square.rake +77 -0
- data/lib/tasks/spree_square_demo.rake +142 -0
- data/spree_square.gemspec +56 -0
- 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
|
data/lib/spree_square.rb
ADDED
|
@@ -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: []
|