solidus_advanced_pricing 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +66 -1
- data/README.md +62 -13
- data/app/decorators/models/solidus_advanced_pricing/spree/price_decorator.rb +21 -1
- data/app/decorators/models/solidus_advanced_pricing/spree/role_decorator.rb +3 -1
- data/app/models/solidus_advanced_pricing/price_selector.rb +3 -1
- data/app/models/solidus_advanced_pricing/price_type.rb +2 -0
- data/app/models/solidus_advanced_pricing/price_type_cache.rb +19 -1
- data/config/locales/en.yml +7 -2
- data/db/migrate/20260923000001_add_role_to_solidus_advanced_pricing_price_types.rb +10 -0
- data/lib/components/admin/solidus_admin/price_types/index/component.rb +4 -0
- data/lib/controllers/admin/solidus_admin/price_types_controller.rb +1 -1
- data/lib/controllers/backend/spree/admin/price_types_controller.rb +1 -1
- data/lib/solidus_advanced_pricing/version.rb +1 -1
- data/lib/views/backend/spree/admin/price_types/_form.html.erb +11 -0
- data/lib/views/backend/spree/admin/price_types/index.html.erb +2 -0
- data/lib/views/backend/spree/admin/prices/_advanced_fields.html.erb +5 -4
- data/solidus_advanced_pricing.gemspec +3 -3
- metadata +6 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1750fd578f0dd63061c1ad728e7418e11bd128ee4bd18d602b58bdc43d6d84c1
|
|
4
|
+
data.tar.gz: e56ded8723862a6ac3513dd4ac9df9a5f074a7a5dfcc41664d595a440c67cbf4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 158337d85d20a1d2099954234f17a1adf6c3e5eef14369a60379bb690edfd9fd42874dd442a17980a89427f1d2d3cfff14f460350ed173cd3bde65a582954ee3
|
|
7
|
+
data.tar.gz: ca2d434c231a0e0ea9d9e9bc88730d39b66455a9296cce9809935337ff7d0b5fd7e54eb35471aed2456e3799a1ec4241a4f4d99db2d6fb7f5660d975061bd09d
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,68 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
All notable changes to this project are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
|
|
5
|
+
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.2.0] - 2026-09-23
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Price types can carry a **default role**. A price with no role of its own inherits its
|
|
12
|
+
type's role, so a type like `employee` can be targeted once instead of on every price.
|
|
13
|
+
A price's own role still wins when set. This is the recommended way to avoid publishing
|
|
14
|
+
a role-named price to everyone.
|
|
15
|
+
- `map` and `promotional` price types are now seeded, bringing the set to six.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **An untyped price could not be created in the admin.** The price type select had no
|
|
20
|
+
blank option, and defaulted to its first entry — so every price created through the UI
|
|
21
|
+
was silently typed `wholesale`. A regression from making `price_type_id` nullable in
|
|
22
|
+
0.1.0.
|
|
23
|
+
- **Validity windows lost their time.** `valid_from` and `valid_to` are datetime columns,
|
|
24
|
+
but the form used Solidus' `.datepicker` class, which is initialised as flatpickr
|
|
25
|
+
without `enableTime` and can only produce a date. Any time an admin set was dropped to
|
|
26
|
+
midnight. Now rendered as native `datetime-local` inputs.
|
|
27
|
+
- Deleting a `Spree::Role` referenced by a price type's default role raised a raw foreign
|
|
28
|
+
key violation instead of being blocked with an error.
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
|
|
32
|
+
- **The price type is now locked once a price exists**, matching how core treats the
|
|
33
|
+
country field. Changing which pricing dimension an existing row belongs to reinterprets
|
|
34
|
+
historical data rather than correcting it.
|
|
35
|
+
|
|
36
|
+
## [0.1.0] - 2026-09-23
|
|
37
|
+
|
|
38
|
+
Initial release.
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- **Price types** — an admin-managed model with a code, name and position. `price_type_id`
|
|
43
|
+
on `spree_prices` is nullable; `NULL` means the untyped base price.
|
|
44
|
+
- **Validity windows** — optional `valid_from` / `valid_to` on each price. `valid_to` is
|
|
45
|
+
exclusive.
|
|
46
|
+
- **Role targeting** — an optional `Spree::Role` per price. Blank means every customer,
|
|
47
|
+
guests included. Role is *eligibility only*, never specificity: a role-targeted price set
|
|
48
|
+
above the untargeted price never applies.
|
|
49
|
+
- **`admin_notes`** — internal commentary on a price, never rendered to customers.
|
|
50
|
+
- A `PriceSelector` and `PricingOptions` registered through
|
|
51
|
+
`Spree::Config.variant_price_selector_class`: filter by currency, type, window and role;
|
|
52
|
+
prefer the country-specific bucket over the any-country fallback; cheapest wins.
|
|
53
|
+
- `Spree::Variant#price_of_type` and `#base_price` for compare-at (strikethrough) display.
|
|
54
|
+
- `Spree::Variant.with_prices` now honours validity windows and role targeting, so a
|
|
55
|
+
variant whose only price has expired no longer counts as purchasable.
|
|
56
|
+
- Full legacy backend admin: price fields, table columns, a price types CRUD, and a
|
|
57
|
+
settings sidebar link. A price types index for `solidus_admin`.
|
|
58
|
+
- `GET /api/variants/:variant_id/prices` — admin-facing; `admin_notes` is exposed only to
|
|
59
|
+
a user who can update the price.
|
|
60
|
+
|
|
61
|
+
### Security
|
|
62
|
+
|
|
63
|
+
- Pricing cache keys include the customer's pricing-relevant roles and a coarse time
|
|
64
|
+
bucket. Without the role component a role-targeted price would be cached and served to
|
|
65
|
+
guests.
|
|
66
|
+
|
|
67
|
+
[0.2.0]: https://github.com/fcpeuro/solidus_advanced_pricing/compare/v0.1.0...v0.2.0
|
|
68
|
+
[0.1.0]: https://github.com/fcpeuro/solidus_advanced_pricing/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -80,8 +80,8 @@ value for `price_of_type`.
|
|
|
80
80
|
|
|
81
81
|
| Column | Type | Meaning |
|
|
82
82
|
|---|---|---|
|
|
83
|
-
| `price_type_id` | bigint, nullable | Belongs to `SolidusAdvancedPricing::PriceType`. `nil` means the untyped base price — exactly as `role_id: nil` means
|
|
84
|
-
| `role_id` | integer, nullable | Belongs to `Spree::Role`. `nil` means
|
|
83
|
+
| `price_type_id` | bigint, nullable | Belongs to `SolidusAdvancedPricing::PriceType`. `nil` means the untyped base price — exactly as `role_id: nil` means "inherit from the type" (see below). |
|
|
84
|
+
| `role_id` | integer, nullable | Belongs to `Spree::Role`. `nil` does **not** mean "public" — it means the price's visibility is inherited from its price type, if any. See [Effective role](#effective-role) below. |
|
|
85
85
|
| `valid_from` | datetime, nullable | Window opens here. `nil` means always open. |
|
|
86
86
|
| `valid_to` | datetime, nullable | Window closes here, **exclusive** — a price is valid up to but not including this instant, so a window ending at midnight and the next one starting at midnight don't both match. `nil` means never closes. |
|
|
87
87
|
| `admin_notes` | text, nullable | Internal only. Never shown to customers; see [API](#api) for exactly who can see it. |
|
|
@@ -89,15 +89,61 @@ value for `price_of_type`.
|
|
|
89
89
|
`price_type_id`, `role_id`, `valid_from` and `valid_to` are all optional. `valid_to`
|
|
90
90
|
must be after `valid_from` when both are set.
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
`SolidusAdvancedPricing::PriceType` also carries a nullable `role_id` of its own —
|
|
93
|
+
the type's *default* role. See [Effective role](#effective-role).
|
|
93
94
|
|
|
94
|
-
|
|
95
|
-
role are independent columns. A price typed `employee` with `role_id` left blank is
|
|
96
|
-
visible to every customer, guests included — the name is just a label for the admin
|
|
97
|
-
UI and reporting; it grants no eligibility by itself. Two of the six seeded types,
|
|
98
|
-
`wholesale` and `employee`, exist specifically to invite this mistake.
|
|
95
|
+
## Effective role
|
|
99
96
|
|
|
100
|
-
|
|
97
|
+
A price's visibility to a customer is never decided by its own `role_id` alone. It is
|
|
98
|
+
decided by its **effective role**:
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
price.role_id || price.price_type&.role_id
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**`nil` on a price's own `role_id` means "inherit from the type," not "explicitly
|
|
105
|
+
public."** If the price has no type, or its type has no default role, the effective
|
|
106
|
+
role is `nil` and the price is visible to everyone, including guests — exactly as
|
|
107
|
+
before this feature existed. But once a price type carries a default role, every
|
|
108
|
+
price of that type inherits it unless the price sets its own `role_id`.
|
|
109
|
+
|
|
110
|
+
The deliberate consequence: **if the `employee` type has a default role, you cannot
|
|
111
|
+
make an employee-typed price public.** Its `role_id` is either blank (inherits the
|
|
112
|
+
type's role) or set to some other role (its own, more specific, targeting) — there is
|
|
113
|
+
no value that means "ignore the type's default and show this to everyone." That
|
|
114
|
+
tradeoff is the point. It turns this gem's biggest footgun — a price typed `employee`
|
|
115
|
+
with no role, visible to every customer including guests — into something a store
|
|
116
|
+
fixes once, on the type, instead of something every price author must remember.
|
|
117
|
+
|
|
118
|
+
Stores are **not** seeded with any type-level defaults; all six seeded types
|
|
119
|
+
(`wholesale`, `sale`, `clearance`, `employee`, `map`, `promotional`) ship with
|
|
120
|
+
`role_id: nil`, because the correct role for, say, `employee` doesn't exist in every
|
|
121
|
+
store. Set it yourself once eligibility should be automatic:
|
|
122
|
+
|
|
123
|
+
```ruby
|
|
124
|
+
employee_role = Spree::Role.find_or_create_by!(name: "employee")
|
|
125
|
+
employee_type = SolidusAdvancedPricing::PriceType.find_by(code: "employee")
|
|
126
|
+
employee_type.update!(role: employee_role)
|
|
127
|
+
|
|
128
|
+
# Every price typed `employee`, existing or future, is now visible only to
|
|
129
|
+
# customers holding the `employee` role -- no per-price role_id required.
|
|
130
|
+
variant.prices.create!(amount: 45, currency: "USD", price_type: employee_type)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## A price type is not access control by default
|
|
134
|
+
|
|
135
|
+
**Setting a price's `price_type` to `employee` does nothing on its own unless the
|
|
136
|
+
type itself carries a default role.** Type and role are independent columns until a
|
|
137
|
+
store links them via the type's `role_id`. A price typed `employee`, with the type
|
|
138
|
+
having no default role and the price's own `role_id` left blank, is visible to every
|
|
139
|
+
customer, guests included — the type name alone is just a label for the admin UI and
|
|
140
|
+
reporting; it grants no eligibility by itself. Two of the six seeded types,
|
|
141
|
+
`wholesale` and `employee`, exist specifically to invite this mistake — and setting a
|
|
142
|
+
default role on the type (above) is the recommended way to close it for good, rather
|
|
143
|
+
than remembering to set `role_id` on every price of that type.
|
|
144
|
+
|
|
145
|
+
If you'd rather target one specific price without touching the type, set `role`
|
|
146
|
+
directly on it:
|
|
101
147
|
|
|
102
148
|
```ruby
|
|
103
149
|
employee_role = Spree::Role.find_or_create_by!(name: "employee")
|
|
@@ -112,7 +158,9 @@ variant.prices.create!(
|
|
|
112
158
|
```
|
|
113
159
|
|
|
114
160
|
With both fields set, a guest or any customer without the `employee` role still sees
|
|
115
|
-
the ordinary price; only a customer holding that role sees $45.
|
|
161
|
+
the ordinary price; only a customer holding that role sees $45. This still works
|
|
162
|
+
exactly as before even once the type has its own default role — an explicit
|
|
163
|
+
`role_id` on the price always wins over the type's.
|
|
116
164
|
|
|
117
165
|
## How a price is chosen
|
|
118
166
|
|
|
@@ -121,9 +169,10 @@ in three steps:
|
|
|
121
169
|
|
|
122
170
|
1. **Filter.** A price survives only if: its currency matches exactly; the current
|
|
123
171
|
time falls inside its validity window (or it has none); and it's visible to the
|
|
124
|
-
customer —
|
|
125
|
-
|
|
126
|
-
|
|
172
|
+
customer — its [effective role](#effective-role) is blank, or the customer holds
|
|
173
|
+
that role. If a price type is pinned (see
|
|
174
|
+
[Admin vs. customer lookups](#admin-vs-customer-lookups) below), non-matching
|
|
175
|
+
types are dropped here too.
|
|
127
176
|
2. **Country specificity.** Among what survives, a price matching the customer's
|
|
128
177
|
country beats every country-agnostic (`country_iso: nil`) price — **even when the
|
|
129
178
|
agnostic price is cheaper.** This matches core Solidus behavior.
|
|
@@ -30,8 +30,23 @@ module SolidusAdvancedPricing
|
|
|
30
30
|
.where(arel_table[:valid_to].eq(nil).or(arel_table[:valid_to].gt(time)))
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
# Tests the *effective* role (price's own role_id, falling back to its
|
|
34
|
+
# type's) via COALESCE over a LEFT JOIN, not just the price's column —
|
|
35
|
+
# a price whose type carries a default role must be excluded from a
|
|
36
|
+
# guest's listing even though the price row itself has role_id: nil.
|
|
37
|
+
# left_joins(:price_type) does not apply PriceType's `kept` default
|
|
38
|
+
# scope (verified empirically), so a retired type's default role still
|
|
39
|
+
# applies to its historical prices.
|
|
33
40
|
base.scope :visible_to_roles, ->(role_ids) {
|
|
34
|
-
|
|
41
|
+
price_types_table = SolidusAdvancedPricing::PriceType.arel_table
|
|
42
|
+
effective_role_id = Arel::Nodes::NamedFunction.new(
|
|
43
|
+
"COALESCE", [arel_table[:role_id], price_types_table[:role_id]]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
condition = effective_role_id.eq(nil)
|
|
47
|
+
condition = condition.or(effective_role_id.in(role_ids)) if role_ids.present?
|
|
48
|
+
|
|
49
|
+
left_joins(:price_type).where(condition)
|
|
35
50
|
}
|
|
36
51
|
|
|
37
52
|
base.scope :for_price_type, ->(price_type) {
|
|
@@ -41,6 +56,11 @@ module SolidusAdvancedPricing
|
|
|
41
56
|
base.after_commit { SolidusAdvancedPricing::PriceTypeCache.clear }
|
|
42
57
|
end
|
|
43
58
|
|
|
59
|
+
# nil on the price means "inherit from the type", not "public".
|
|
60
|
+
def effective_role_id
|
|
61
|
+
role_id || SolidusAdvancedPricing::PriceTypeCache.role_id_for(price_type_id)
|
|
62
|
+
end
|
|
63
|
+
|
|
44
64
|
private
|
|
45
65
|
|
|
46
66
|
def valid_to_after_valid_from
|
|
@@ -12,7 +12,9 @@ module SolidusAdvancedPricing
|
|
|
12
12
|
private
|
|
13
13
|
|
|
14
14
|
def prevent_destroying_referenced_role
|
|
15
|
-
|
|
15
|
+
referenced = ::Spree::Price.with_discarded.exists?(role_id: id) ||
|
|
16
|
+
SolidusAdvancedPricing::PriceType.with_discarded.exists?(role_id: id)
|
|
17
|
+
return unless referenced
|
|
16
18
|
|
|
17
19
|
errors.add(:base, :referenced_by_prices)
|
|
18
20
|
throw :abort
|
|
@@ -49,7 +49,9 @@ module SolidusAdvancedPricing
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def visible_to?(price, customer_role_ids)
|
|
52
|
-
|
|
52
|
+
effective = price.effective_role_id
|
|
53
|
+
|
|
54
|
+
effective.nil? || customer_role_ids.include?(effective)
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
# Country is specificity, not competition: a country-specific price beats the
|
|
@@ -21,6 +21,8 @@ module SolidusAdvancedPricing
|
|
|
21
21
|
foreign_key: :price_type_id,
|
|
22
22
|
inverse_of: :price_type
|
|
23
23
|
|
|
24
|
+
belongs_to :role, class_name: "::Spree::Role", optional: true
|
|
25
|
+
|
|
24
26
|
# Not `dependent: :restrict_with_error` — that check is default-scoped and
|
|
25
27
|
# misses discarded prices, which then trip the FK on DELETE.
|
|
26
28
|
before_destroy :prevent_destroying_referenced_type
|
|
@@ -6,10 +6,15 @@ module SolidusAdvancedPricing
|
|
|
6
6
|
# and, later, by Spree::Price.
|
|
7
7
|
module PriceTypeCache
|
|
8
8
|
class << self
|
|
9
|
+
# Union with type-level defaults: a role that appears only on a price type
|
|
10
|
+
# (never directly on a price) must still narrow in, or a customer holding
|
|
11
|
+
# only that role would be filtered out before selection ever runs.
|
|
9
12
|
def pricing_role_ids
|
|
10
13
|
return @pricing_role_ids if defined?(@pricing_role_ids)
|
|
11
14
|
|
|
12
|
-
@pricing_role_ids =
|
|
15
|
+
@pricing_role_ids = (
|
|
16
|
+
::Spree::Price.distinct.pluck(:role_id) + PriceType.with_discarded.distinct.pluck(:role_id)
|
|
17
|
+
).compact.uniq
|
|
13
18
|
end
|
|
14
19
|
|
|
15
20
|
# id => position, for the selector's tie-breaker. A handful of rows that
|
|
@@ -35,10 +40,23 @@ module SolidusAdvancedPricing
|
|
|
35
40
|
ids_by_code[code.to_s]
|
|
36
41
|
end
|
|
37
42
|
|
|
43
|
+
# id => role_id, for the effective-role fallback. with_discarded matters —
|
|
44
|
+
# a retired type's default role must still apply to historical prices.
|
|
45
|
+
def role_ids
|
|
46
|
+
return @role_ids if defined?(@role_ids)
|
|
47
|
+
|
|
48
|
+
@role_ids = PriceType.with_discarded.pluck(:id, :role_id).to_h
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def role_id_for(price_type_id)
|
|
52
|
+
role_ids[price_type_id]
|
|
53
|
+
end
|
|
54
|
+
|
|
38
55
|
def clear
|
|
39
56
|
remove_instance_variable(:@pricing_role_ids) if defined?(@pricing_role_ids)
|
|
40
57
|
remove_instance_variable(:@positions) if defined?(@positions)
|
|
41
58
|
remove_instance_variable(:@ids_by_code) if defined?(@ids_by_code)
|
|
59
|
+
remove_instance_variable(:@role_ids) if defined?(@role_ids)
|
|
42
60
|
end
|
|
43
61
|
end
|
|
44
62
|
end
|
data/config/locales/en.yml
CHANGED
|
@@ -6,7 +6,10 @@ en:
|
|
|
6
6
|
solidus_advanced_pricing:
|
|
7
7
|
price_types: "Price Types"
|
|
8
8
|
all_customers: "All customers"
|
|
9
|
-
|
|
9
|
+
base_price: "Base price (no type)"
|
|
10
|
+
price_type_hint: "Leave as Base price for the standard price. A type is a label for the admin and reporting — it grants no eligibility on its own."
|
|
11
|
+
role_hint: "Leave blank to inherit the price type's default role, if it has one; otherwise visible to all customers. A price type name alone grants no access control — an employee-only price needs this field, or the type, set."
|
|
12
|
+
price_type_role_hint: "Prices of this type default to this role unless a price sets its own. Leaving a price's own role blank means it inherits this default — it does not make that price public."
|
|
10
13
|
admin_notes_hint: "Internal only. Never shown to customers."
|
|
11
14
|
validity: "Validity"
|
|
12
15
|
solidus_admin:
|
|
@@ -22,6 +25,8 @@ en:
|
|
|
22
25
|
one: "Price Type"
|
|
23
26
|
other: "Price Types"
|
|
24
27
|
attributes:
|
|
28
|
+
solidus_advanced_pricing/price_type:
|
|
29
|
+
role_id: "Default role"
|
|
25
30
|
spree/price:
|
|
26
31
|
price_type_id: "Price Type"
|
|
27
32
|
role_id: "Visible to role"
|
|
@@ -41,4 +46,4 @@ en:
|
|
|
41
46
|
spree/role:
|
|
42
47
|
attributes:
|
|
43
48
|
base:
|
|
44
|
-
referenced_by_prices: "This role is used to target one or more prices and cannot be deleted."
|
|
49
|
+
referenced_by_prices: "This role is used to target one or more prices or price types and cannot be deleted."
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class AddRoleToSolidusAdvancedPricingPriceTypes < ActiveRecord::Migration[7.0]
|
|
4
|
+
def change
|
|
5
|
+
# :integer, not :bigint — spree_roles.id is int and MySQL rejects mismatched FK precision.
|
|
6
|
+
add_column :solidus_advanced_pricing_price_types, :role_id, :integer
|
|
7
|
+
add_index :solidus_advanced_pricing_price_types, :role_id
|
|
8
|
+
add_foreign_key :solidus_advanced_pricing_price_types, :spree_roles, column: :role_id, on_delete: :restrict
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -7,7 +7,7 @@ module SolidusAdmin
|
|
|
7
7
|
def resource_class = SolidusAdvancedPricing::PriceType
|
|
8
8
|
|
|
9
9
|
def permitted_resource_params
|
|
10
|
-
params.require(:price_type).permit(:name, :code, :position)
|
|
10
|
+
params.require(:price_type).permit(:name, :code, :position, :role_id)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
# ResourcesController derives these from resource_class.model_name, which for
|
|
@@ -14,7 +14,7 @@ module Spree
|
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def permitted_resource_params
|
|
17
|
-
params.require(:price_type).permit(:name, :code, :position)
|
|
17
|
+
params.require(:price_type).permit(:name, :code, :position, :role_id)
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
# ResourceController derives these from model_class.model_name, which for a
|
|
@@ -17,4 +17,15 @@
|
|
|
17
17
|
<%= f.number_field :position, class: 'fullwidth' %>
|
|
18
18
|
<% end %>
|
|
19
19
|
</div>
|
|
20
|
+
<div class="col-6">
|
|
21
|
+
<%= f.field_container :role do %>
|
|
22
|
+
<%= f.label :role_id, Spree::Role.model_name.human %>
|
|
23
|
+
<%= f.collection_select :role_id,
|
|
24
|
+
Spree::Role.order(:name),
|
|
25
|
+
:id, :name,
|
|
26
|
+
{ include_blank: t('solidus_advanced_pricing.all_customers') },
|
|
27
|
+
{ class: 'custom-select fullwidth' } %>
|
|
28
|
+
<span class="field-hint"><%= t('solidus_advanced_pricing.price_type_role_hint') %></span>
|
|
29
|
+
<% end %>
|
|
30
|
+
</div>
|
|
20
31
|
</div>
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
<th><%= SolidusAdvancedPricing::PriceType.human_attribute_name(:name) %></th>
|
|
13
13
|
<th><%= SolidusAdvancedPricing::PriceType.human_attribute_name(:code) %></th>
|
|
14
14
|
<th><%= SolidusAdvancedPricing::PriceType.human_attribute_name(:position) %></th>
|
|
15
|
+
<th><%= SolidusAdvancedPricing::PriceType.human_attribute_name(:role_id) %></th>
|
|
15
16
|
<th class="actions"></th>
|
|
16
17
|
</tr>
|
|
17
18
|
</thead>
|
|
@@ -21,6 +22,7 @@
|
|
|
21
22
|
<td><%= price_type.name %></td>
|
|
22
23
|
<td><%= price_type.code %></td>
|
|
23
24
|
<td><%= price_type.position %></td>
|
|
25
|
+
<td><%= price_type.role&.name || t('solidus_advanced_pricing.all_customers') %></td>
|
|
24
26
|
<td class="actions">
|
|
25
27
|
<%= link_to_edit price_type, no_text: true %>
|
|
26
28
|
<%= link_to_delete price_type, no_text: true %>
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
<%= f.collection_select :price_type_id,
|
|
6
6
|
SolidusAdvancedPricing::PriceType.ordered,
|
|
7
7
|
:id, :name,
|
|
8
|
-
{},
|
|
9
|
-
{ class: 'custom-select fullwidth' } %>
|
|
8
|
+
{ include_blank: t('solidus_advanced_pricing.base_price') },
|
|
9
|
+
{ class: 'custom-select fullwidth', disabled: !f.object.new_record? } %>
|
|
10
|
+
<span class="field-hint"><%= t('solidus_advanced_pricing.price_type_hint') %></span>
|
|
10
11
|
<% end %>
|
|
11
12
|
</div>
|
|
12
13
|
|
|
@@ -25,14 +26,14 @@
|
|
|
25
26
|
<div class="col-2">
|
|
26
27
|
<%= f.field_container :valid_from do %>
|
|
27
28
|
<%= f.label :valid_from %>
|
|
28
|
-
<%= f.
|
|
29
|
+
<%= f.datetime_local_field :valid_from, class: 'fullwidth' %>
|
|
29
30
|
<% end %>
|
|
30
31
|
</div>
|
|
31
32
|
|
|
32
33
|
<div class="col-2">
|
|
33
34
|
<%= f.field_container :valid_to do %>
|
|
34
35
|
<%= f.label :valid_to %>
|
|
35
|
-
<%= f.
|
|
36
|
+
<%= f.datetime_local_field :valid_to, class: 'fullwidth' %>
|
|
36
37
|
<% end %>
|
|
37
38
|
</div>
|
|
38
39
|
|
|
@@ -10,12 +10,12 @@ Gem::Specification.new do |spec|
|
|
|
10
10
|
|
|
11
11
|
spec.summary = "Price types, validity windows and role targeting for Solidus prices"
|
|
12
12
|
spec.description = "Adds an admin-managed price type, an optional validity window, optional single-role targeting and internal admin notes to Spree::Price, selected through Solidus' own variant_price_selector_class seam."
|
|
13
|
-
spec.homepage = "https://github.com/
|
|
13
|
+
spec.homepage = "https://github.com/fcpeuro/solidus_advanced_pricing#readme"
|
|
14
14
|
spec.license = "BSD-3-Clause"
|
|
15
15
|
|
|
16
16
|
spec.metadata["homepage_uri"] = spec.homepage
|
|
17
|
-
spec.metadata["source_code_uri"] = "https://github.com/
|
|
18
|
-
spec.metadata["changelog_uri"] = "https://github.com/
|
|
17
|
+
spec.metadata["source_code_uri"] = "https://github.com/fcpeuro/solidus_advanced_pricing"
|
|
18
|
+
spec.metadata["changelog_uri"] = "https://github.com/fcpeuro/solidus_advanced_pricing/blob/main/CHANGELOG.md"
|
|
19
19
|
|
|
20
20
|
spec.required_ruby_version = ">= 3.1"
|
|
21
21
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: solidus_advanced_pricing
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick McMorran
|
|
@@ -178,6 +178,7 @@ files:
|
|
|
178
178
|
- db/migrate/20260915000001_create_solidus_advanced_pricing_price_types.rb
|
|
179
179
|
- db/migrate/20260915000002_seed_solidus_advanced_pricing_price_types.rb
|
|
180
180
|
- db/migrate/20260915000003_add_advanced_pricing_to_spree_prices.rb
|
|
181
|
+
- db/migrate/20260923000001_add_role_to_solidus_advanced_pricing_price_types.rb
|
|
181
182
|
- docs/superpowers/specs/2026-09-14-solidus-advanced-pricing-design.md
|
|
182
183
|
- lib/components/admin/solidus_admin/price_types/index/component.rb
|
|
183
184
|
- lib/controllers/admin/solidus_admin/price_types_controller.rb
|
|
@@ -201,13 +202,13 @@ files:
|
|
|
201
202
|
- lib/views/backend/spree/admin/prices/_advanced_columns.html.erb
|
|
202
203
|
- lib/views/backend/spree/admin/prices/_advanced_fields.html.erb
|
|
203
204
|
- solidus_advanced_pricing.gemspec
|
|
204
|
-
homepage: https://github.com/
|
|
205
|
+
homepage: https://github.com/fcpeuro/solidus_advanced_pricing#readme
|
|
205
206
|
licenses:
|
|
206
207
|
- BSD-3-Clause
|
|
207
208
|
metadata:
|
|
208
|
-
homepage_uri: https://github.com/
|
|
209
|
-
source_code_uri: https://github.com/
|
|
210
|
-
changelog_uri: https://github.com/
|
|
209
|
+
homepage_uri: https://github.com/fcpeuro/solidus_advanced_pricing#readme
|
|
210
|
+
source_code_uri: https://github.com/fcpeuro/solidus_advanced_pricing
|
|
211
|
+
changelog_uri: https://github.com/fcpeuro/solidus_advanced_pricing/blob/main/CHANGELOG.md
|
|
211
212
|
rdoc_options: []
|
|
212
213
|
require_paths:
|
|
213
214
|
- lib
|