spree_square 0.1.3 → 0.2.1
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 +4 -4
- data/CHANGELOG.md +46 -0
- data/README.md +26 -0
- data/app/controllers/spree/admin/square_tax_rates_controller.rb +28 -0
- data/app/models/spree_square/tax_category_mapping.rb +24 -0
- data/app/models/spree_square/tax_combination.rb +31 -0
- data/app/models/spree_square/tax_mapping.rb +24 -0
- data/app/services/spree_square/catalog_importer.rb +12 -9
- data/app/services/spree_square/catalog_object_mapper.rb +177 -0
- data/app/views/spree/admin/square_tax_rates/index.html.erb +5 -0
- data/config/initializers/spree_admin_square_navigation.rb +16 -0
- data/config/initializers/spree_admin_square_tables.rb +48 -0
- data/config/routes.rb +1 -0
- data/db/migrate/20260820180001_create_spree_square_tax_mappings.rb +22 -0
- data/db/migrate/20260820180002_create_spree_square_tax_category_mappings.rb +23 -0
- data/db/migrate/20260820190001_create_spree_square_tax_combinations.rb +22 -0
- data/lib/spree_square/version.rb +1 -1
- data/lib/tasks/spree_square.rake +82 -0
- metadata +9 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 25d852608d74d0163f8216bfe8daaa44b0d0cff76612deb563aaee475fbf6fc7
|
|
4
|
+
data.tar.gz: d5d1b302b8713ef27a7b89a58490225b2903a3e6f263ba23efc6822983facb90
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c16da8f9c347d08bc694092c3babb2bda2a96c483d1972ce83b24f578893715357f8d2fa36f6a476969b9589abab4b635e5ba73776e373a8e50073c7a3db6c20
|
|
7
|
+
data.tar.gz: 8c6681d1a46fefe6fce793f03d6f6a3a668618f3e958691dd27fbb8b8f25dae061ffebdb38daccbc57d1174d700342e9124307e7dc66916a8c55d837295c3e63
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.2.1
|
|
6
|
+
|
|
7
|
+
Fixes a real bug in `v0.2.0`, found running it against production traffic (not caught by specs —
|
|
8
|
+
sandbox/local dev has no registered webhook to race against): the concurrency fix for the composite
|
|
9
|
+
tax category race (see `v0.2.0`'s own changelog entry) was itself Postgres-only
|
|
10
|
+
(`pg_advisory_xact_lock`), breaking outright on the gem's own SQLite/MySQL-tested dummy app, and
|
|
11
|
+
production actually hit the race it was meant to prevent — this rake task's own `CatalogImporter.call`
|
|
12
|
+
ran concurrently with a real `catalog.version.updated` webhook Square's own `update_item_taxes` call
|
|
13
|
+
fired back mid-run, leaving 44 products correctly on one "Sales Tax" category and 1 stray product on a
|
|
14
|
+
duplicate with its own duplicate rate (same 8% amount either way — no wrong tax charged, but a real
|
|
15
|
+
data duplication). Replaced with `SpreeSquare::TaxCombination`, a real unique index raced via
|
|
16
|
+
`create_or_find_by!` — portable across every adapter this gem supports. `v0.2.0` is yanked; upgrade
|
|
17
|
+
directly to this version.
|
|
18
|
+
|
|
19
|
+
## 0.2.0
|
|
20
|
+
|
|
21
|
+
- **Sales tax, sourced from Square's own catalog tax config.** Square's Catalog API has no concept
|
|
22
|
+
of jurisdiction, so this is two pieces working together: a new rake task
|
|
23
|
+
(`spree_square:ensure_tax_zone`) sets up a Spree::Zone matching the store's own StockLocation
|
|
24
|
+
state (re-derived live, not hardcoded), and the catalog importer now pulls Square's `TAX`
|
|
25
|
+
catalog objects (`CatalogImporter`/`CatalogObjectMapper#map_tax`) plus each item's `tax_ids`
|
|
26
|
+
(`#resolve_tax_category`), mirroring them into `Spree::TaxRate`/`Spree::TaxCategory` — Spree's
|
|
27
|
+
own built-in tax-calculation engine does the actual per-order math, unchanged. An item carrying
|
|
28
|
+
more than one Square tax at once gets its own composite `Spree::TaxCategory` with one
|
|
29
|
+
`Spree::TaxRate` per constituent tax (`SpreeSquare::TaxMapping`/`TaxCategoryMapping`), rather
|
|
30
|
+
than forcing a single flat rate — correct for the general case, though this project's own
|
|
31
|
+
catalog only ever needs one. Disabling a tax in Square soft-deletes every `Spree::TaxRate` it
|
|
32
|
+
backs; re-enabling restores them. Reuses the existing `catalog.version.updated` webhook — no new
|
|
33
|
+
subscription needed.
|
|
34
|
+
- New rake task `spree_square:setup_demo_tax` — creates a real "Sales Tax" object in Square
|
|
35
|
+
Sandbox, batch-attaches it to every item via `update_item_taxes`, and (Spree-side, since Square
|
|
36
|
+
has no concept of a delivery fee at all) applies the same tax category to every shipping method.
|
|
37
|
+
- New read-only admin page, "Square Tax Rates" (`/admin/square_tax_rates`), mirroring the existing
|
|
38
|
+
Square Orders/Webhooks pages.
|
|
39
|
+
- Fixed a real bug found while building this: `Spree::TaxRate`'s `has_one :calculator, dependent:
|
|
40
|
+
destroy` performs a REAL (hard) destroy even when the owning TaxRate is only soft-deleted —
|
|
41
|
+
acts_as_paranoid only intercepts the TaxRate's own row, not its dependent-destroy callback
|
|
42
|
+
chain. Re-enabling a previously-disabled tax rebuilds the lost calculator explicitly; without
|
|
43
|
+
this, the very next order recalculation against that rate raised
|
|
44
|
+
(`Spree::TaxRate#calculator` was blank).
|
|
45
|
+
- Documented, not fixed here (pre-existing, discovered during manual verification): a store's
|
|
46
|
+
connected OAuth credential (`SpreeSquare::Credential`) may not carry `ITEMS_WRITE` if it was
|
|
47
|
+
authorized before this or `seed_demo_menu`'s scope needs existed — both item-write rake tasks
|
|
48
|
+
need it. Reconnecting via the admin's OAuth flow picks up the current scope list; until then,
|
|
49
|
+
`setup_demo_tax` explicitly bypasses the connected credential in favor of `SQUARE_ACCESS_TOKEN`.
|
|
50
|
+
|
|
5
51
|
## 0.1.3
|
|
6
52
|
|
|
7
53
|
- New demo menu content: a "Pizzas" category with 5 standalone pizzas plus a
|
data/README.md
CHANGED
|
@@ -70,6 +70,32 @@ The original single-tenant path: generate a token yourself from the Developer Da
|
|
|
70
70
|
store that hasn't connected via OAuth — convenient for local development, but not something
|
|
71
71
|
Square allows for real multi-merchant production use.
|
|
72
72
|
|
|
73
|
+
## Sales tax
|
|
74
|
+
|
|
75
|
+
Tax rates are configured in Square (the same place you manage the menu), not in Spree's admin —
|
|
76
|
+
this extension keeps `Spree::TaxRate`/`Spree::TaxCategory` in sync with whatever `TAX` catalog
|
|
77
|
+
objects and item `tax_ids` you set up in Square. **Square's Catalog API has no concept of
|
|
78
|
+
jurisdiction**, so one one-time Spree-side step is still required:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
bin/rails spree_square:ensure_tax_zone
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
This creates a `Spree::Zone` matching your store's own `Spree::StockLocation` state (re-derived
|
|
85
|
+
live — re-run it if that address ever changes). From there, create a `TAX` object in Square and
|
|
86
|
+
attach it to your taxable items (via the Square dashboard, or the Catalog API's
|
|
87
|
+
`update_item_taxes`) — the next catalog sync (webhook or `spree_square:import_catalog`) picks it
|
|
88
|
+
up automatically. `spree_square:setup_demo_tax` does both of the Square-side steps for you against
|
|
89
|
+
a fresh Sandbox catalog, for a working demo without leaving the terminal.
|
|
90
|
+
|
|
91
|
+
Delivery/shipping fees are entirely outside Square's model — assign a `tax_category` to your
|
|
92
|
+
`Spree::ShippingMethod` records directly (`setup_demo_tax` does this too) if you want them taxed
|
|
93
|
+
the same way.
|
|
94
|
+
|
|
95
|
+
An item carrying more than one Square tax at once (rare — most stores have exactly one) gets its
|
|
96
|
+
own composite `Spree::TaxCategory` combining all of them; Spree's own tax-calculation engine sums
|
|
97
|
+
every rate that shares an item's category, so no special handling is needed at checkout time.
|
|
98
|
+
|
|
73
99
|
## Developing
|
|
74
100
|
|
|
75
101
|
1. Create a dummy app
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
# Read-only support/diagnostic view — no create/edit/destroy, this is
|
|
4
|
+
# visibility into what Square's own tax config has synced into Spree,
|
|
5
|
+
# not a place to change it (edits happen in Square's own dashboard).
|
|
6
|
+
# Mirrors SquareOrderMappingsController exactly.
|
|
7
|
+
class SquareTaxRatesController < ResourceController
|
|
8
|
+
def model_class
|
|
9
|
+
Spree::TaxRate
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
# `scope`, not `collection` — collection is ResourceController's full
|
|
15
|
+
# ransack+pagination pipeline (search_collection.result...pagy), and
|
|
16
|
+
# overriding it directly skips building @search, which is exactly
|
|
17
|
+
# what `render_table`'s search_form_for needs. `scope` is the one
|
|
18
|
+
# documented extension point for narrowing the base query before
|
|
19
|
+
# ransack/pagination run. Scoped here so this page only ever shows
|
|
20
|
+
# rates this extension actually created (via
|
|
21
|
+
# SpreeSquare::TaxCategoryMapping), not any rate a store admin might
|
|
22
|
+
# separately hand-create in the regular Spree tax-rates admin page.
|
|
23
|
+
def scope
|
|
24
|
+
Spree::TaxRate.where(id: SpreeSquare::TaxCategoryMapping.select(:tax_rate_id))
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module SpreeSquare
|
|
2
|
+
# Join row: "in this Spree::TaxCategory, this Square tax is represented by
|
|
3
|
+
# this Spree::TaxRate." A composite category (an item carrying more than
|
|
4
|
+
# one Square tax at once) has one row per constituent tax. See
|
|
5
|
+
# CatalogObjectMapper#resolve_tax_category for how this is used to
|
|
6
|
+
# find-or-create the right category for an item's exact set of Square tax
|
|
7
|
+
# ids, and TaxMapping for why this can't just be a belongs_to on
|
|
8
|
+
# TaxMapping itself (one Square tax can back rates in more than one
|
|
9
|
+
# category).
|
|
10
|
+
class TaxCategoryMapping < Spree.base_class
|
|
11
|
+
self.table_name = 'spree_square_tax_category_mappings'
|
|
12
|
+
|
|
13
|
+
# `with_deleted` (mirrors Spree::TaxRate's own belongs_to :tax_category)
|
|
14
|
+
# — without it, a soft-deleted (disabled-in-Square) rate becomes
|
|
15
|
+
# unreachable through this association, which would break re-enabling
|
|
16
|
+
# it later: CatalogObjectMapper#sync_enabled_state! needs to find and
|
|
17
|
+
# restore the very row it previously destroyed.
|
|
18
|
+
belongs_to :tax_category, class_name: 'Spree::TaxCategory'
|
|
19
|
+
belongs_to :tax_mapping, class_name: 'SpreeSquare::TaxMapping'
|
|
20
|
+
belongs_to :tax_rate, -> { with_deleted }, class_name: 'Spree::TaxRate'
|
|
21
|
+
|
|
22
|
+
validates :tax_mapping_id, uniqueness: { scope: :tax_category_id }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module SpreeSquare
|
|
2
|
+
# One row per distinct combination of Square tax ids an item carries at
|
|
3
|
+
# once, keyed by `signature` (sorted, comma-joined SpreeSquare::TaxMapping
|
|
4
|
+
# ids) — the race-safe find-or-create key `create_or_find_by!` needs. See
|
|
5
|
+
# CatalogObjectMapper#composite_tax_category for why this exists: without
|
|
6
|
+
# a real unique index to race against, two concurrent imports resolving
|
|
7
|
+
# the same combination for the first time could each create their own
|
|
8
|
+
# Spree::TaxCategory.
|
|
9
|
+
class TaxCombination < Spree.base_class
|
|
10
|
+
self.table_name = 'spree_square_tax_combinations'
|
|
11
|
+
|
|
12
|
+
belongs_to :tax_category, class_name: 'Spree::TaxCategory'
|
|
13
|
+
|
|
14
|
+
# Deliberately NO `uniqueness: true` here — that's a validation-layer
|
|
15
|
+
# SELECT-then-INSERT check with exactly the same race this table exists
|
|
16
|
+
# to close. `create_or_find_by!` (see CatalogObjectMapper#
|
|
17
|
+
# composite_tax_category) depends on the real DB-level unique index
|
|
18
|
+
# (the migration's `add_index ... unique: true`) raising
|
|
19
|
+
# ActiveRecord::RecordNotUnique on the losing INSERT, which is the one
|
|
20
|
+
# thing it explicitly rescues — a uniqueness validation raises
|
|
21
|
+
# RecordInvalid instead, which it does not, and defeats the whole
|
|
22
|
+
# point (confirmed live: adding this validation back made the second,
|
|
23
|
+
# non-racing call in this file's own idempotency spec raise instead of
|
|
24
|
+
# transparently finding the row).
|
|
25
|
+
validates :signature, presence: true
|
|
26
|
+
|
|
27
|
+
def self.signature_for(tax_mapping_ids)
|
|
28
|
+
tax_mapping_ids.sort.join(',')
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module SpreeSquare
|
|
2
|
+
# Mirrors one Square CatalogTax object — a plain flat percentage + an
|
|
3
|
+
# inclusion type, category-agnostic. Square's Catalog API has no concept
|
|
4
|
+
# of jurisdiction/zone at all, so this row never points at a
|
|
5
|
+
# Spree::TaxRate directly: which Spree::TaxCategory (and therefore which
|
|
6
|
+
# Spree::TaxRate) a given Square tax participates in depends on which
|
|
7
|
+
# *combination* of Square taxes each item carries — see
|
|
8
|
+
# TaxCategoryMapping and CatalogObjectMapper#resolve_tax_category.
|
|
9
|
+
#
|
|
10
|
+
# `square_version` is Square's optimistic-concurrency token — same
|
|
11
|
+
# stale-check pattern as CatalogMapping, guards against out-of-order or
|
|
12
|
+
# duplicate webhook delivery re-processing older data over newer.
|
|
13
|
+
class TaxMapping < Spree.base_class
|
|
14
|
+
self.table_name = 'spree_square_tax_mappings'
|
|
15
|
+
|
|
16
|
+
has_many :tax_category_mappings, class_name: 'SpreeSquare::TaxCategoryMapping', dependent: :destroy
|
|
17
|
+
|
|
18
|
+
validates :square_tax_id, presence: true, uniqueness: true
|
|
19
|
+
|
|
20
|
+
def stale?(incoming_version)
|
|
21
|
+
square_version.present? && incoming_version.present? && incoming_version <= square_version
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
module SpreeSquare
|
|
2
|
-
# Full catalog import: pulls every ITEM + CATEGORY + MODIFIER_LIST
|
|
3
|
-
# Square (with related objects — images, referenced categories —
|
|
4
|
-
# via `include_related_objects`) and upserts them into Spree via
|
|
5
|
-
# CatalogObjectMapper. Categories
|
|
6
|
-
# items so an item can resolve its `category_id` /
|
|
7
|
-
# an already-mapped record.
|
|
2
|
+
# Full catalog import: pulls every ITEM + CATEGORY + MODIFIER_LIST + TAX
|
|
3
|
+
# from Square (with related objects — images, referenced categories —
|
|
4
|
+
# inlined via `include_related_objects`) and upserts them into Spree via
|
|
5
|
+
# CatalogObjectMapper. Categories, modifier lists, and taxes are imported
|
|
6
|
+
# before items so an item can resolve its `category_id` /
|
|
7
|
+
# `modifier_list_info` / `tax_ids` to an already-mapped record.
|
|
8
8
|
class CatalogImporter
|
|
9
|
-
Result = Struct.new(:categories_count, :modifier_lists_count, :items_count, keyword_init: true)
|
|
9
|
+
Result = Struct.new(:categories_count, :modifier_lists_count, :taxes_count, :items_count, keyword_init: true)
|
|
10
10
|
|
|
11
11
|
def self.call = new.call
|
|
12
12
|
|
|
@@ -17,14 +17,17 @@ module SpreeSquare
|
|
|
17
17
|
by_type = objects.group_by(&:type)
|
|
18
18
|
categories = by_type.fetch('CATEGORY', [])
|
|
19
19
|
modifier_lists = by_type.fetch('MODIFIER_LIST', [])
|
|
20
|
+
taxes = by_type.fetch('TAX', [])
|
|
20
21
|
items = by_type.fetch('ITEM', [])
|
|
21
22
|
|
|
22
23
|
mapper = CatalogObjectMapper.new(related_objects_by_id: related_by_id)
|
|
23
24
|
categories.each { |category| mapper.map_category(category) }
|
|
24
25
|
modifier_lists.each { |list| mapper.map_modifier_list(list) }
|
|
26
|
+
taxes.each { |tax| mapper.map_tax(tax) }
|
|
25
27
|
items.each { |item| mapper.map_item(item) }
|
|
26
28
|
|
|
27
|
-
Result.new(categories_count: categories.size, modifier_lists_count: modifier_lists.size,
|
|
29
|
+
Result.new(categories_count: categories.size, modifier_lists_count: modifier_lists.size,
|
|
30
|
+
taxes_count: taxes.size, items_count: items.size)
|
|
28
31
|
end
|
|
29
32
|
|
|
30
33
|
private
|
|
@@ -39,7 +42,7 @@ module SpreeSquare
|
|
|
39
42
|
|
|
40
43
|
loop do
|
|
41
44
|
response = client.catalog.search(
|
|
42
|
-
object_types: %w[ITEM CATEGORY MODIFIER_LIST],
|
|
45
|
+
object_types: %w[ITEM CATEGORY MODIFIER_LIST TAX],
|
|
43
46
|
include_related_objects: true,
|
|
44
47
|
cursor: cursor
|
|
45
48
|
)
|
|
@@ -75,6 +75,46 @@ module SpreeSquare
|
|
|
75
75
|
modifier
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# Upserts a Square CatalogTax into SpreeSquare::TaxMapping — a pure
|
|
79
|
+
# mirror (percentage/inclusion/enabled), category-agnostic. Which
|
|
80
|
+
# Spree::TaxCategory (and therefore Spree::TaxRate) this tax actually
|
|
81
|
+
# backs is resolved per-item in map_item, since that depends on which
|
|
82
|
+
# *combination* of taxes each item carries — see resolve_tax_category.
|
|
83
|
+
def map_tax(square_object)
|
|
84
|
+
data = square_object.tax_data
|
|
85
|
+
mapping = SpreeSquare::TaxMapping.find_or_initialize_by(square_tax_id: square_object.id)
|
|
86
|
+
return mapping if mapping.persisted? && mapping.stale?(square_object.version)
|
|
87
|
+
|
|
88
|
+
was_enabled = mapping.persisted? ? mapping.enabled : nil
|
|
89
|
+
mapping.name = data.name.presence || 'Square Tax'
|
|
90
|
+
mapping.percentage = data.percentage.presence&.to_d || 0
|
|
91
|
+
mapping.included_in_price = (data.inclusion_type == 'INCLUSIVE')
|
|
92
|
+
mapping.enabled = data.enabled != false
|
|
93
|
+
mapping.square_version = square_object.version
|
|
94
|
+
mapping.last_synced_at = Time.current
|
|
95
|
+
mapping.save!
|
|
96
|
+
|
|
97
|
+
# Enable/disable first — a re-enable rebuilds the rate's calculator
|
|
98
|
+
# (see sync_enabled_state!'s own comment for why that's needed), which
|
|
99
|
+
# the amount/inclusion sync below depends on being present.
|
|
100
|
+
sync_enabled_state!(mapping, was_enabled)
|
|
101
|
+
|
|
102
|
+
# Already-materialized (and currently live) rates mirror any
|
|
103
|
+
# percentage/inclusion/name change immediately — Square is the source
|
|
104
|
+
# of truth here, these rows are never hand-edited in Spree. A rate
|
|
105
|
+
# that's still disabled is skipped: no live TaxRate exists for it to
|
|
106
|
+
# update, and the next enable will rebuild it fresh from this mapping
|
|
107
|
+
# anyway.
|
|
108
|
+
mapping.tax_category_mappings.includes(:tax_rate).each do |tcm|
|
|
109
|
+
rate = tcm.tax_rate
|
|
110
|
+
next unless rate && !rate.paranoia_destroyed?
|
|
111
|
+
|
|
112
|
+
rate.update!(amount: mapping.percentage / 100.0, included_in_price: mapping.included_in_price, name: mapping.name)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
mapping
|
|
116
|
+
end
|
|
117
|
+
|
|
78
118
|
def map_item(square_object)
|
|
79
119
|
data = square_object.item_data
|
|
80
120
|
mapping = SpreeSquare::CatalogMapping.find_or_initialize_by(
|
|
@@ -91,6 +131,7 @@ module SpreeSquare
|
|
|
91
131
|
)
|
|
92
132
|
product.name = data.name.presence || 'Untitled item'
|
|
93
133
|
product.description = data.description
|
|
134
|
+
product.tax_category = resolve_tax_category(data)
|
|
94
135
|
product.save!
|
|
95
136
|
|
|
96
137
|
publish!(product)
|
|
@@ -222,5 +263,141 @@ module SpreeSquare
|
|
|
222
263
|
base = name.presence || 'item'
|
|
223
264
|
"#{base.parameterize}-#{square_id.downcase}"
|
|
224
265
|
end
|
|
266
|
+
|
|
267
|
+
# Enable/disable is the one thing that can't just be "update the row" —
|
|
268
|
+
# a disabled Square tax must stop applying everywhere it appears, and a
|
|
269
|
+
# re-enabled one must resume applying with no re-import of the items
|
|
270
|
+
# that reference it. Spree::TaxRate is acts_as_paranoid, so soft-delete/
|
|
271
|
+
# restore is exactly "stop/resume applying" — Spree's own
|
|
272
|
+
# TaxRate.potential_rates_for_zone already excludes deleted rows via its
|
|
273
|
+
# default scope, no other code needs to know about this.
|
|
274
|
+
def sync_enabled_state!(mapping, was_enabled)
|
|
275
|
+
return if was_enabled == mapping.enabled
|
|
276
|
+
|
|
277
|
+
mapping.tax_category_mappings.each do |tcm|
|
|
278
|
+
rate = tcm.tax_rate
|
|
279
|
+
next unless rate
|
|
280
|
+
|
|
281
|
+
if mapping.enabled
|
|
282
|
+
next unless rate.paranoia_destroyed?
|
|
283
|
+
|
|
284
|
+
rate.restore(recursive: true)
|
|
285
|
+
# Spree::TaxRate's `has_one :calculator, dependent: :destroy` is a
|
|
286
|
+
# REAL (hard) destroy even when the owning TaxRate is only
|
|
287
|
+
# soft-deleted — acts_as_paranoid intercepts the TaxRate's own row,
|
|
288
|
+
# not its dependent-destroy callbacks. So a restored rate has no
|
|
289
|
+
# calculator left to restore; rebuild it, or `TaxRate.adjust`'s
|
|
290
|
+
# `delegate :compute, to: :calculator` raises on the next order
|
|
291
|
+
# recalculation.
|
|
292
|
+
rate.calculator_type = 'Spree::Calculator::DefaultTax' if rate.calculator.nil?
|
|
293
|
+
rate.save!
|
|
294
|
+
elsif !rate.paranoia_destroyed?
|
|
295
|
+
rate.destroy
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Resolves an item's exact set of Square tax_ids to the Spree::TaxCategory
|
|
301
|
+
# that represents that combination — creating it (and the underlying
|
|
302
|
+
# Spree::TaxRate(s), one per constituent Square tax) the first time this
|
|
303
|
+
# exact combination is seen. See TaxCategoryMapping for why a composite
|
|
304
|
+
# category can need more than one row here.
|
|
305
|
+
def resolve_tax_category(item_data)
|
|
306
|
+
square_tax_ids = Array(item_data.tax_ids)
|
|
307
|
+
return non_taxable_category if square_tax_ids.empty?
|
|
308
|
+
|
|
309
|
+
tax_mappings = SpreeSquare::TaxMapping.where(square_tax_id: square_tax_ids).to_a
|
|
310
|
+
# Item references a tax id this store hasn't synced yet (import order,
|
|
311
|
+
# or the tax was deleted in Square) — fall back to non-taxable rather
|
|
312
|
+
# than guessing; a later re-import (taxes always run before items)
|
|
313
|
+
# will resolve it correctly once the tax itself is known.
|
|
314
|
+
return non_taxable_category if tax_mappings.empty?
|
|
315
|
+
|
|
316
|
+
category = composite_tax_category(tax_mappings)
|
|
317
|
+
ensure_tax_rates!(category, tax_mappings)
|
|
318
|
+
category
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# A real bug found running this against production: the same catalog
|
|
322
|
+
# import can run concurrently more than once (this rake task's own
|
|
323
|
+
# `CatalogImporter.call` racing a real `catalog.version.updated`
|
|
324
|
+
# webhook that Square's own `update_item_taxes` call fires back mid-run
|
|
325
|
+
# — sandbox/local dev, with no registered webhook, never exercises
|
|
326
|
+
# this). Two processes each finding "no matching category yet" and
|
|
327
|
+
# independently creating one left two "Sales Tax" categories with
|
|
328
|
+
# duplicate rates. `SpreeSquare::TaxCombination#signature` is a real
|
|
329
|
+
# unique index — `create_or_find_by!` races against it exactly the way
|
|
330
|
+
# Rails intends (portable across Postgres/MySQL/SQLite, unlike a
|
|
331
|
+
# database-specific advisory lock): the losing side's block-created
|
|
332
|
+
# Spree::TaxCategory rolls back with it (create_or_find_by! wraps the
|
|
333
|
+
# attempt in its own transaction) and it transparently re-finds what
|
|
334
|
+
# the winner created instead.
|
|
335
|
+
def composite_tax_category(tax_mappings)
|
|
336
|
+
signature = SpreeSquare::TaxCombination.signature_for(tax_mappings.map(&:id))
|
|
337
|
+
|
|
338
|
+
# `create_or_find_by!`'s block runs while *building* the record, before
|
|
339
|
+
# the create attempt that its own race-safety depends on — so it can
|
|
340
|
+
# run more than once even with no real race at all (confirmed live: a
|
|
341
|
+
# second, non-concurrent call in the same process re-runs this block
|
|
342
|
+
# and only then discovers the TaxCombination already exists). The
|
|
343
|
+
# block itself must therefore be safe to run repeatedly — plain
|
|
344
|
+
# `Spree::TaxCategory.create!` isn't, since core validates `name`
|
|
345
|
+
# uniqueness and a second run raises `RecordInvalid` before this
|
|
346
|
+
# method's own race-safety ever gets a chance to matter.
|
|
347
|
+
combination = SpreeSquare::TaxCombination.create_or_find_by!(signature: signature) do |c|
|
|
348
|
+
name = tax_mappings.map(&:name).join(' + ').presence || 'Square Tax'
|
|
349
|
+
c.tax_category = Spree::TaxCategory.find_by(name: name) || Spree::TaxCategory.create!(name: name)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
combination.tax_category
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def ensure_tax_rates!(category, tax_mappings)
|
|
356
|
+
zone = tax_zone!
|
|
357
|
+
|
|
358
|
+
tax_mappings.each do |tm|
|
|
359
|
+
next if SpreeSquare::TaxCategoryMapping.exists?(tax_category_id: category.id, tax_mapping_id: tm.id)
|
|
360
|
+
|
|
361
|
+
rate = Spree::TaxRate.create!(
|
|
362
|
+
zone: zone,
|
|
363
|
+
tax_category: category,
|
|
364
|
+
name: tm.name,
|
|
365
|
+
amount: (tm.percentage || 0) / 100.0,
|
|
366
|
+
included_in_price: tm.included_in_price,
|
|
367
|
+
calculator_type: 'Spree::Calculator::DefaultTax'
|
|
368
|
+
)
|
|
369
|
+
SpreeSquare::TaxCategoryMapping.create!(tax_category: category, tax_mapping: tm, tax_rate: rate)
|
|
370
|
+
rescue ActiveRecord::RecordNotUnique
|
|
371
|
+
# A concurrent import created this exact (category, tax) row first
|
|
372
|
+
# (same class of race `composite_tax_category` guards against, just
|
|
373
|
+
# a narrower window) — the orphaned rate this attempt already
|
|
374
|
+
# created never got a join row and is safe to drop; the other
|
|
375
|
+
# side's row is what matters.
|
|
376
|
+
rate&.destroy
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def non_taxable_category
|
|
381
|
+
@non_taxable_category ||= Spree::TaxCategory.find_by(name: 'Non-taxable') ||
|
|
382
|
+
Spree::TaxCategory.create!(name: 'Non-taxable')
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# The store's own tax Zone, set up once via `spree_square:ensure_tax_zone`
|
|
386
|
+
# (Square's Catalog API has no jurisdiction concept to sync — this is
|
|
387
|
+
# entirely Spree-side). Raises rather than silently skipping: importing
|
|
388
|
+
# a real tax with no zone to attach it to is a setup error, not a
|
|
389
|
+
# recoverable no-op.
|
|
390
|
+
def tax_zone!
|
|
391
|
+
@tax_zone ||= begin
|
|
392
|
+
state = Spree::StockLocation.find_by(default: true)&.state
|
|
393
|
+
zone = state && Spree::Zone.where(kind: 'state')
|
|
394
|
+
.joins(:zone_members)
|
|
395
|
+
.where(spree_zone_members: { zoneable_type: 'Spree::State', zoneable_id: state.id })
|
|
396
|
+
.first
|
|
397
|
+
raise "No tax Zone found for the store's state — run `bin/rails spree_square:ensure_tax_zone` first." if zone.blank?
|
|
398
|
+
|
|
399
|
+
zone
|
|
400
|
+
end
|
|
401
|
+
end
|
|
225
402
|
end
|
|
226
403
|
end
|
|
@@ -22,4 +22,20 @@ Rails.application.config.after_initialize do
|
|
|
22
22
|
position: 67,
|
|
23
23
|
active: -> { controller_name == 'square_oauth' },
|
|
24
24
|
if: -> { can?(:manage, SpreeSquare::Credential) }
|
|
25
|
+
|
|
26
|
+
# Position 75 — confirmed free at the time this was added by checking
|
|
27
|
+
# every nav initializer across this project: spree_square itself (65-67,
|
|
28
|
+
# above), spree_doordash (68-70), spree_menu_chat (71-72), spree_loyalty
|
|
29
|
+
# (73-74), plus spree_host's own host-local
|
|
30
|
+
# admin_square_modifier_lists_navigation.rb (73 — a pre-existing
|
|
31
|
+
# collision with spree_loyalty's 73, not this extension's to fix).
|
|
32
|
+
# Confirm the actual current highest position the same way before adding
|
|
33
|
+
# another nav item anywhere in this project.
|
|
34
|
+
Spree.admin.navigation.sidebar.add :square_tax_rates,
|
|
35
|
+
label: 'Square Tax Rates',
|
|
36
|
+
url: :admin_square_tax_rates_path,
|
|
37
|
+
icon: 'percentage',
|
|
38
|
+
position: 75,
|
|
39
|
+
active: -> { controller_name == 'square_tax_rates' },
|
|
40
|
+
if: -> { can?(:manage, Spree::TaxRate) }
|
|
25
41
|
end
|
|
@@ -92,4 +92,52 @@ Rails.application.config.after_initialize do
|
|
|
92
92
|
filterable: false,
|
|
93
93
|
default: true,
|
|
94
94
|
position: 50
|
|
95
|
+
|
|
96
|
+
# new_resource: false — read-only, same reason as every table above (no
|
|
97
|
+
# :new/:create route; only: [:index] in config/routes.rb).
|
|
98
|
+
Spree.admin.tables.register(:square_tax_rates, model_class: Spree::TaxRate,
|
|
99
|
+
search_param: :name_cont, new_resource: false)
|
|
100
|
+
|
|
101
|
+
Spree.admin.tables.square_tax_rates.add :name,
|
|
102
|
+
label: :name,
|
|
103
|
+
type: :string,
|
|
104
|
+
sortable: true,
|
|
105
|
+
filterable: true,
|
|
106
|
+
default: true,
|
|
107
|
+
position: 10
|
|
108
|
+
|
|
109
|
+
Spree.admin.tables.square_tax_rates.add :amount_percentage,
|
|
110
|
+
label: :rate,
|
|
111
|
+
type: :string,
|
|
112
|
+
sortable: false,
|
|
113
|
+
filterable: false,
|
|
114
|
+
default: true,
|
|
115
|
+
position: 20,
|
|
116
|
+
method: ->(rate) { "#{rate.amount_percentage}%" }
|
|
117
|
+
|
|
118
|
+
Spree.admin.tables.square_tax_rates.add :zone,
|
|
119
|
+
label: :zone,
|
|
120
|
+
type: :string,
|
|
121
|
+
sortable: false,
|
|
122
|
+
filterable: false,
|
|
123
|
+
default: true,
|
|
124
|
+
position: 30,
|
|
125
|
+
method: ->(rate) { rate.zone&.name }
|
|
126
|
+
|
|
127
|
+
Spree.admin.tables.square_tax_rates.add :tax_category,
|
|
128
|
+
label: :tax_category,
|
|
129
|
+
type: :string,
|
|
130
|
+
sortable: false,
|
|
131
|
+
filterable: false,
|
|
132
|
+
default: true,
|
|
133
|
+
position: 40,
|
|
134
|
+
method: ->(rate) { rate.tax_category&.name }
|
|
135
|
+
|
|
136
|
+
Spree.admin.tables.square_tax_rates.add :updated_at,
|
|
137
|
+
label: :last_synced,
|
|
138
|
+
type: :datetime,
|
|
139
|
+
sortable: true,
|
|
140
|
+
filterable: false,
|
|
141
|
+
default: true,
|
|
142
|
+
position: 50
|
|
95
143
|
end
|
data/config/routes.rb
CHANGED
|
@@ -13,6 +13,7 @@ Spree::Core::Engine.add_routes do
|
|
|
13
13
|
namespace :admin do
|
|
14
14
|
resources :square_order_mappings, only: [:index]
|
|
15
15
|
resources :square_webhook_events, only: [:index]
|
|
16
|
+
resources :square_tax_rates, only: [:index]
|
|
16
17
|
|
|
17
18
|
# Self-service OAuth connection (see Spree::Admin::SquareOauthController).
|
|
18
19
|
# Explicit named routes rather than `resource :square_oauth` — the
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class CreateSpreeSquareTaxMappings < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :spree_square_tax_mappings do |t|
|
|
4
|
+
t.string :square_tax_id, null: false
|
|
5
|
+
t.string :name
|
|
6
|
+
# Mirrors what Square itself stores: a plain decimal percentage
|
|
7
|
+
# (e.g. 8.0, not 0.08) — kept alongside the derived Spree::TaxRate(s)
|
|
8
|
+
# so re-imports can detect a no-op without re-deriving from a rate.
|
|
9
|
+
t.decimal :percentage, precision: 8, scale: 5
|
|
10
|
+
t.boolean :included_in_price, default: false, null: false
|
|
11
|
+
t.boolean :enabled, default: true, null: false
|
|
12
|
+
# Square's optimistic-concurrency token — same stale-check pattern as
|
|
13
|
+
# CatalogMapping, guards against out-of-order webhook delivery.
|
|
14
|
+
t.bigint :square_version
|
|
15
|
+
t.datetime :last_synced_at
|
|
16
|
+
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
add_index :spree_square_tax_mappings, :square_tax_id, unique: true
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class CreateSpreeSquareTaxCategoryMappings < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
# A single Square tax can back tax rates in more than one Spree tax
|
|
4
|
+
# category — e.g. item A carries only "Sales Tax", item B carries
|
|
5
|
+
# "Sales Tax" + "Bottle Tax" together, so "Sales Tax" needs its own
|
|
6
|
+
# Spree::TaxRate under each of the two resulting composite categories.
|
|
7
|
+
# This join records, for one (tax_category, square tax) pair, which
|
|
8
|
+
# Spree::TaxRate row represents it — the lookup CatalogObjectMapper
|
|
9
|
+
# uses to find-or-create the right composite Spree::TaxCategory for an
|
|
10
|
+
# item's exact set of Square tax ids, without any schema change to
|
|
11
|
+
# core's spree_tax_categories/spree_tax_rates tables.
|
|
12
|
+
create_table :spree_square_tax_category_mappings do |t|
|
|
13
|
+
t.references :tax_category, null: false, foreign_key: { to_table: :spree_tax_categories }
|
|
14
|
+
t.references :tax_mapping, null: false, foreign_key: { to_table: :spree_square_tax_mappings }
|
|
15
|
+
t.references :tax_rate, null: false, foreign_key: { to_table: :spree_tax_rates }
|
|
16
|
+
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
add_index :spree_square_tax_category_mappings, %i[tax_category_id tax_mapping_id],
|
|
21
|
+
unique: true, name: 'index_square_tax_category_mappings_on_category_and_tax'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class CreateSpreeSquareTaxCombinations < ActiveRecord::Migration[8.1]
|
|
2
|
+
def change
|
|
3
|
+
# Resolves the race `create_composite_tax_category!` used to have: two
|
|
4
|
+
# concurrent imports (this rake task's own CatalogImporter.call racing a
|
|
5
|
+
# real catalog.version.updated webhook Square fires back mid-run —
|
|
6
|
+
# confirmed live against production, never exercised by sandbox/local
|
|
7
|
+
# dev with no registered webhook) each finding "no matching category
|
|
8
|
+
# yet" and independently creating one. `signature` is a unique,
|
|
9
|
+
# deterministic key for one exact combination of Square tax ids
|
|
10
|
+
# (sorted, joined) — `create_or_find_by!` against it is a portable
|
|
11
|
+
# (Postgres/MySQL/SQLite) race-safe find-or-create, unlike a
|
|
12
|
+
# Postgres-only advisory lock.
|
|
13
|
+
create_table :spree_square_tax_combinations do |t|
|
|
14
|
+
t.string :signature, null: false
|
|
15
|
+
t.references :tax_category, null: false, foreign_key: { to_table: :spree_tax_categories }
|
|
16
|
+
|
|
17
|
+
t.timestamps
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
add_index :spree_square_tax_combinations, :signature, unique: true
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/spree_square/version.rb
CHANGED
data/lib/tasks/spree_square.rake
CHANGED
|
@@ -74,4 +74,86 @@ namespace :spree_square do
|
|
|
74
74
|
puts '---'
|
|
75
75
|
puts 'Set SQUARE_WEBHOOK_SIGNATURE_KEY in .env to the value above, then recreate the web container.'
|
|
76
76
|
end
|
|
77
|
+
|
|
78
|
+
desc "Phase 8 M1: ensure a state-scoped tax Zone exists for the store's own StockLocation state"
|
|
79
|
+
task ensure_tax_zone: :environment do
|
|
80
|
+
stock_location = Spree::StockLocation.find_by(default: true)
|
|
81
|
+
abort 'No default Spree::StockLocation found.' if stock_location.blank?
|
|
82
|
+
|
|
83
|
+
state = stock_location.state
|
|
84
|
+
abort "StockLocation ##{stock_location.id} (#{stock_location.name}) has no state set — set one before running this task." if state.blank?
|
|
85
|
+
|
|
86
|
+
# Named/keyed off the state itself (not hardcoded "OH") and re-derived
|
|
87
|
+
# from StockLocation on every run — if that address ever moves to a
|
|
88
|
+
# different state, re-running this task points the zone at the new one
|
|
89
|
+
# instead of silently leaving a stale zone behind.
|
|
90
|
+
zone = Spree::Zone.find_or_initialize_by(name: "#{state.abbr} Sales Tax")
|
|
91
|
+
zone.kind = 'state'
|
|
92
|
+
zone.description = "Sales tax zone for #{state.name} — tracks Spree::StockLocation's own state; see spree_square:ensure_tax_zone." if zone.has_attribute?(:description)
|
|
93
|
+
zone.save!
|
|
94
|
+
zone.state_ids = [state.id]
|
|
95
|
+
|
|
96
|
+
puts "Zone ##{zone.id} '#{zone.name}' now contains exactly: #{zone.states.pluck(:abbr).join(', ')}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
desc 'Phase 8 M4: create a real 8% Sales Tax object in Square Sandbox, attach it to every item, and tax delivery fees to match'
|
|
100
|
+
task setup_demo_tax: :environment do
|
|
101
|
+
# credential: nil forces the SQUARE_ACCESS_TOKEN env fallback instead of
|
|
102
|
+
# this store's connected OAuth credential — the OAuth connection was
|
|
103
|
+
# authorized without ITEMS_WRITE (confirmed live: its scopes list is
|
|
104
|
+
# MERCHANT_PROFILE_READ/ITEMS_READ/INVENTORY_*/ORDERS_*/PAYMENTS_*, no
|
|
105
|
+
# ITEMS_WRITE), the same scope seed_demo_menu.rake's item-creation
|
|
106
|
+
# calls need. A one-time catalog-admin task like this one reasonably
|
|
107
|
+
# uses the broader sandbox token rather than re-running the OAuth
|
|
108
|
+
# consent flow just to add a scope.
|
|
109
|
+
client = SpreeSquare::Client.new(credential: nil)
|
|
110
|
+
|
|
111
|
+
puts 'Creating "Sales Tax" (8.0%) catalog object in Square...'
|
|
112
|
+
response = client.catalog.batch_upsert(
|
|
113
|
+
idempotency_key: SecureRandom.uuid,
|
|
114
|
+
batches: [{
|
|
115
|
+
objects: [{
|
|
116
|
+
type: 'TAX',
|
|
117
|
+
id: '#tax-sales-tax',
|
|
118
|
+
tax_data: {
|
|
119
|
+
name: 'Sales Tax',
|
|
120
|
+
calculation_phase: 'TAX_SUBTOTAL_PHASE',
|
|
121
|
+
inclusion_type: 'ADDITIVE',
|
|
122
|
+
percentage: '8.0',
|
|
123
|
+
applies_to_custom_amounts: true,
|
|
124
|
+
enabled: true
|
|
125
|
+
}
|
|
126
|
+
}]
|
|
127
|
+
}]
|
|
128
|
+
)
|
|
129
|
+
tax_id = response.id_mappings.find { |m| m.client_object_id == '#tax-sales-tax' }&.object_id_
|
|
130
|
+
abort 'Square did not return an id for the new tax object.' if tax_id.blank?
|
|
131
|
+
puts "Created tax #{tax_id}"
|
|
132
|
+
|
|
133
|
+
item_ids = client.catalog.search(object_types: ['ITEM']).objects.to_a.map(&:id)
|
|
134
|
+
abort 'No items found in the Square catalog — run spree_square:seed_demo_menu first.' if item_ids.empty?
|
|
135
|
+
|
|
136
|
+
puts "Attaching to #{item_ids.size} item(s)..."
|
|
137
|
+
client.catalog.update_item_taxes(item_ids: item_ids, taxes_to_enable: [tax_id])
|
|
138
|
+
|
|
139
|
+
puts 'Waiting for Square search index to catch up...'
|
|
140
|
+
sleep 20
|
|
141
|
+
|
|
142
|
+
puts 'Importing into Spree...'
|
|
143
|
+
result = SpreeSquare::CatalogImporter.call
|
|
144
|
+
puts "Imported #{result.taxes_count} tax(es) and re-synced #{result.items_count} item(s)."
|
|
145
|
+
|
|
146
|
+
tax_mapping = SpreeSquare::TaxMapping.find_by(square_tax_id: tax_id)
|
|
147
|
+
tax_category = tax_mapping&.tax_category_mappings&.first&.tax_category
|
|
148
|
+
if tax_category.blank?
|
|
149
|
+
puts 'Warning: could not resolve the resulting Spree::TaxCategory — did any item actually import with this tax attached?'
|
|
150
|
+
else
|
|
151
|
+
# Square's Catalog API has no concept of a delivery fee at all — this
|
|
152
|
+
# part is Spree-only, applying the same tax category to every
|
|
153
|
+
# shipping method per the confirmed decision (delivery fee taxed the
|
|
154
|
+
# same as food).
|
|
155
|
+
count = Spree::ShippingMethod.update_all(tax_category_id: tax_category.id)
|
|
156
|
+
puts "Set tax_category '#{tax_category.name}' on #{count} shipping method(s): #{Spree::ShippingMethod.pluck(:name).join(', ')}"
|
|
157
|
+
end
|
|
158
|
+
end
|
|
77
159
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_square
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Amit Solanki
|
|
@@ -121,6 +121,7 @@ files:
|
|
|
121
121
|
- app/assets/images/.keep
|
|
122
122
|
- app/controllers/spree/admin/square_oauth_controller.rb
|
|
123
123
|
- app/controllers/spree/admin/square_order_mappings_controller.rb
|
|
124
|
+
- app/controllers/spree/admin/square_tax_rates_controller.rb
|
|
124
125
|
- app/controllers/spree/admin/square_webhook_events_controller.rb
|
|
125
126
|
- app/controllers/spree_square/webhooks_controller.rb
|
|
126
127
|
- app/javascript/spree_square/application.js
|
|
@@ -141,6 +142,9 @@ files:
|
|
|
141
142
|
- app/models/spree_square/modifier_list.rb
|
|
142
143
|
- app/models/spree_square/order_mapping.rb
|
|
143
144
|
- app/models/spree_square/product_modifier_list.rb
|
|
145
|
+
- app/models/spree_square/tax_category_mapping.rb
|
|
146
|
+
- app/models/spree_square/tax_combination.rb
|
|
147
|
+
- app/models/spree_square/tax_mapping.rb
|
|
144
148
|
- app/models/spree_square/taxon_mapping.rb
|
|
145
149
|
- app/models/spree_square/webhook_event.rb
|
|
146
150
|
- app/serializers/spree_square/line_item_serializer.rb
|
|
@@ -161,6 +165,7 @@ files:
|
|
|
161
165
|
- app/subscribers/spree_square/order_completed_subscriber.rb
|
|
162
166
|
- app/views/spree/admin/square_oauth/show.html.erb
|
|
163
167
|
- app/views/spree/admin/square_order_mappings/index.html.erb
|
|
168
|
+
- app/views/spree/admin/square_tax_rates/index.html.erb
|
|
164
169
|
- app/views/spree/admin/square_webhook_events/index.html.erb
|
|
165
170
|
- bin/importmap
|
|
166
171
|
- bin/rails
|
|
@@ -181,6 +186,9 @@ files:
|
|
|
181
186
|
- db/migrate/20260809160004_create_spree_square_line_item_modifiers.rb
|
|
182
187
|
- db/migrate/20260809170001_create_spree_square_order_mappings.rb
|
|
183
188
|
- db/migrate/20260810180001_create_spree_square_credentials.rb
|
|
189
|
+
- db/migrate/20260820180001_create_spree_square_tax_mappings.rb
|
|
190
|
+
- db/migrate/20260820180002_create_spree_square_tax_category_mappings.rb
|
|
191
|
+
- db/migrate/20260820190001_create_spree_square_tax_combinations.rb
|
|
184
192
|
- lib/generators/spree_square/install/install_generator.rb
|
|
185
193
|
- lib/spree_square.rb
|
|
186
194
|
- lib/spree_square/configuration.rb
|