spree_cm_commissioner 2.9.7 → 2.9.8
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/Gemfile.lock +1 -1
- data/app/controllers/concerns/spree_cm_commissioner/order_concern.rb +5 -0
- data/app/controllers/spree/api/v2/storefront/draft_orders_controller.rb +2 -1
- data/app/controllers/spree/api/v2/tenant/draft_orders_controller.rb +2 -1
- data/app/interactors/spree_cm_commissioner/create_ticket.rb +7 -3
- data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +0 -1
- data/app/models/concerns/spree_cm_commissioner/purchase_limitable.rb +59 -0
- data/app/models/concerns/spree_cm_commissioner/variant_options_concern.rb +0 -1
- data/app/models/spree_cm_commissioner/line_item_decorator.rb +4 -3
- data/app/models/spree_cm_commissioner/product_decorator.rb +1 -0
- data/app/models/spree_cm_commissioner/variant_options.rb +0 -4
- data/app/serializers/spree/v2/storefront/product_serializer_decorator.rb +4 -0
- data/app/serializers/spree/v2/storefront/variant_serializer_decorator.rb +2 -1
- data/app/serializers/spree/v2/tenant/product_serializer.rb +5 -0
- data/app/serializers/spree/v2/tenant/variant_serializer.rb +2 -1
- data/app/services/spree_cm_commissioner/draft_order/create.rb +13 -4
- data/app/services/spree_cm_commissioner/line_items/validate_purchase_limits.rb +97 -0
- data/app/services/spree_cm_commissioner/load_test/generate_booking_ticket_data.rb +4 -4
- data/config/locales/en.yml +12 -1
- data/config/locales/km.yml +16 -5
- data/db/migrate/20260730000000_add_purchase_limit_scopes_to_spree_orders.rb +22 -0
- data/lib/spree_cm_commissioner/test_helper/factories/option_type_factory.rb +0 -6
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8eb692e530b34906dfca4d757e6382b9b643e18760fd38ba31507ceb24bb687b
|
|
4
|
+
data.tar.gz: 782a651003a7817fb539def0a19697d434978c51f7f48c0f2184b48fe87c1260
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '090263c66bf5f79ea0edfcf6602185b0d80120f33308b2d0193cacf3bb2ebc5036543f1e1000b4e823a243a31cd3dc2eaf3e4b8772c6717d16891554b3da96a0'
|
|
7
|
+
data.tar.gz: 3540311c234819a354e154a532b7745e91ebf1faf7b15bcc40a5a6def80ea069d3820759b328decb2328f6ad60190a12686d5c23c0951c864ee89215160fd4a1
|
data/Gemfile.lock
CHANGED
|
@@ -12,6 +12,7 @@ module SpreeCmCommissioner
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
@spree_current_order.last_ip_address = request.remote_ip
|
|
15
|
+
@spree_current_order.device_id = device_id if device_id.present?
|
|
15
16
|
@spree_current_order
|
|
16
17
|
end
|
|
17
18
|
|
|
@@ -25,5 +26,9 @@ module SpreeCmCommissioner
|
|
|
25
26
|
state: params[:state]
|
|
26
27
|
)
|
|
27
28
|
end
|
|
29
|
+
|
|
30
|
+
def device_id
|
|
31
|
+
params[:device_id].presence
|
|
32
|
+
end
|
|
28
33
|
end
|
|
29
34
|
end
|
|
@@ -15,7 +15,8 @@ module Spree
|
|
|
15
15
|
line_items: draft_order_line_items,
|
|
16
16
|
user: spree_current_user,
|
|
17
17
|
store: current_store,
|
|
18
|
-
currency: current_currency
|
|
18
|
+
currency: current_currency,
|
|
19
|
+
options: { device_id: params[:device_id], last_ip_address: request.remote_ip }
|
|
19
20
|
)
|
|
20
21
|
|
|
21
22
|
if result.success?
|
|
@@ -117,12 +117,15 @@ module SpreeCmCommissioner
|
|
|
117
117
|
end
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
-
def ticket_params
|
|
120
|
+
def ticket_params # rubocop:disable Metrics/AbcSize
|
|
121
121
|
{
|
|
122
122
|
name: context.params[:name],
|
|
123
123
|
price: context.params[:price],
|
|
124
124
|
compare_at_price: context.params[:compare_at_price],
|
|
125
|
-
|
|
125
|
+
max_quantity_per_order: context.params[:max_quantity_per_order],
|
|
126
|
+
max_quantity_per_account: context.params[:max_quantity_per_account],
|
|
127
|
+
max_quantity_per_device: context.params[:max_quantity_per_device],
|
|
128
|
+
max_quantity_per_ip: context.params[:max_quantity_per_ip],
|
|
126
129
|
available_on: context.params[:available_on],
|
|
127
130
|
discontinue_on: context.params[:discontinue_on],
|
|
128
131
|
description: context.params[:description],
|
|
@@ -132,7 +135,8 @@ module SpreeCmCommissioner
|
|
|
132
135
|
shipping_category_id: context.params[:shipping_category_id],
|
|
133
136
|
option_type_ids: context.params[:option_type_ids],
|
|
134
137
|
allow_anonymous_booking: context.params[:allow_anonymous_booking],
|
|
135
|
-
allow_self_check_in: context.params[:allow_self_check_in]
|
|
138
|
+
allow_self_check_in: context.params[:allow_self_check_in],
|
|
139
|
+
purchasable_on: context.params[:purchasable_on] || 'both'
|
|
136
140
|
}
|
|
137
141
|
end
|
|
138
142
|
end
|
|
@@ -29,7 +29,6 @@ module SpreeCmCommissioner
|
|
|
29
29
|
'duration-in-seconds' => 'integer',
|
|
30
30
|
'payment-option' => 'payment_option',
|
|
31
31
|
'delivery-option' => 'delivery_option',
|
|
32
|
-
'max-quantity-per-order' => 'integer',
|
|
33
32
|
'due-date' => 'integer',
|
|
34
33
|
'month' => 'integer',
|
|
35
34
|
'number-of-adults' => 'integer',
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Product-level purchase caps per buyer identity (order, account, device, IP).
|
|
2
|
+
# Stored as public metadata and enforced at cart time by
|
|
3
|
+
# SpreeCmCommissioner::LineItems::ValidatePurchaseLimits.
|
|
4
|
+
module SpreeCmCommissioner
|
|
5
|
+
module PurchaseLimitable
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
PURCHASE_LIMIT_KEYS = %i[
|
|
9
|
+
max_quantity_per_order
|
|
10
|
+
max_quantity_per_account
|
|
11
|
+
max_quantity_per_device
|
|
12
|
+
max_quantity_per_ip
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
POSITIVE_INTEGER = { only_integer: true, greater_than: 0, message: :must_be_positive_integer }.freeze
|
|
16
|
+
|
|
17
|
+
included do
|
|
18
|
+
store_public_metadata :max_quantity_per_order, :integer
|
|
19
|
+
store_public_metadata :max_quantity_per_account, :integer
|
|
20
|
+
store_public_metadata :max_quantity_per_device, :integer
|
|
21
|
+
store_public_metadata :max_quantity_per_ip, :integer
|
|
22
|
+
|
|
23
|
+
validates :max_quantity_per_order, numericality: POSITIVE_INTEGER, allow_nil: true
|
|
24
|
+
validates :max_quantity_per_account, numericality: POSITIVE_INTEGER, allow_nil: true
|
|
25
|
+
validates :max_quantity_per_device, numericality: POSITIVE_INTEGER, allow_nil: true
|
|
26
|
+
validates :max_quantity_per_ip, numericality: POSITIVE_INTEGER, allow_nil: true
|
|
27
|
+
|
|
28
|
+
validate :validate_max_quantity_per_account
|
|
29
|
+
validate :validate_max_quantity_per_device
|
|
30
|
+
|
|
31
|
+
before_validation :normalize_purchase_limits
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# A blank cap means "unset". store_public_metadata writes the raw value and its
|
|
37
|
+
# :integer reader casts with to_i, so a form-submitted "" would otherwise read back
|
|
38
|
+
# as 0 and fail both the numericality check and the metadata type check.
|
|
39
|
+
def normalize_purchase_limits
|
|
40
|
+
return if public_metadata.blank?
|
|
41
|
+
|
|
42
|
+
public_metadata.slice(*PURCHASE_LIMIT_KEYS.map(&:to_s)).each do |key, value|
|
|
43
|
+
public_metadata[key] = nil if value.blank?
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def validate_max_quantity_per_account
|
|
48
|
+
return unless max_quantity_per_account.to_i.positive? && allow_anonymous_booking?
|
|
49
|
+
|
|
50
|
+
errors.add(:base, I18n.t('purchase_limit.account_cap_requires_named_booking'))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def validate_max_quantity_per_device
|
|
54
|
+
return unless max_quantity_per_device.to_i.positive? && !purchasable_on_app?
|
|
55
|
+
|
|
56
|
+
errors.add(:base, I18n.t('purchase_limit.device_cap_requires_app_only'))
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -32,7 +32,7 @@ module SpreeCmCommissioner
|
|
|
32
32
|
base.before_save :update_vendor_id
|
|
33
33
|
|
|
34
34
|
base.before_create :add_due_date, if: :subscription?
|
|
35
|
-
base.validate :
|
|
35
|
+
base.validate :ensure_not_exceed_purchase_limits, if: -> { product.present? }
|
|
36
36
|
base.validate :ensure_all_guests_have_blocks, if: :required_seat_selection?
|
|
37
37
|
|
|
38
38
|
base.whitelisted_ransackable_associations |= %w[guests order]
|
|
@@ -290,8 +290,9 @@ module SpreeCmCommissioner
|
|
|
290
290
|
|
|
291
291
|
private
|
|
292
292
|
|
|
293
|
-
def
|
|
294
|
-
|
|
293
|
+
def ensure_not_exceed_purchase_limits
|
|
294
|
+
result = SpreeCmCommissioner::LineItems::ValidatePurchaseLimits.call(line_item: self)
|
|
295
|
+
errors.add(:quantity, result.error.to_s) unless result.success?
|
|
295
296
|
end
|
|
296
297
|
|
|
297
298
|
def ensure_all_guests_have_blocks
|
|
@@ -11,6 +11,7 @@ module SpreeCmCommissioner
|
|
|
11
11
|
base.include SpreeCmCommissioner::Integrations::IntegrationMappable
|
|
12
12
|
base.include SpreeCmCommissioner::StoreMetadata
|
|
13
13
|
base.include SpreeCmCommissioner::HomepageSectionRelatableConcern
|
|
14
|
+
base.include SpreeCmCommissioner::PurchaseLimitable
|
|
14
15
|
|
|
15
16
|
base.delegate :is_open_dated, :is_open_dated?, to: :trip, allow_nil: true
|
|
16
17
|
|
|
@@ -81,10 +81,6 @@ module SpreeCmCommissioner
|
|
|
81
81
|
@delivery_option ||= option_value_name_for(option_type_name: 'delivery-option')
|
|
82
82
|
end
|
|
83
83
|
|
|
84
|
-
def max_quantity_per_order
|
|
85
|
-
@max_quantity_per_order ||= option_value_name_for(option_type_name: 'max-quantity-per-order')&.to_i
|
|
86
|
-
end
|
|
87
|
-
|
|
88
84
|
def due_date
|
|
89
85
|
@due_date ||= option_value_name_for(option_type_name: 'due-date')&.to_i
|
|
90
86
|
end
|
|
@@ -54,6 +54,10 @@ module Spree
|
|
|
54
54
|
base.cache_options store: nil
|
|
55
55
|
|
|
56
56
|
base.attribute :action_button, &:action_button
|
|
57
|
+
base.attribute :max_quantity_per_order, &:max_quantity_per_order
|
|
58
|
+
base.attribute :max_quantity_per_account, &:max_quantity_per_account
|
|
59
|
+
base.attribute :max_quantity_per_device, &:max_quantity_per_device
|
|
60
|
+
base.attribute :max_quantity_per_ip, &:max_quantity_per_ip
|
|
57
61
|
end
|
|
58
62
|
end
|
|
59
63
|
end
|
|
@@ -5,9 +5,10 @@ module Spree
|
|
|
5
5
|
def self.prepended(base)
|
|
6
6
|
base.attributes :need_confirmation, :product_type, :kyc, :allow_anonymous_booking,
|
|
7
7
|
:reminder_in_hours, :start_time, :delivery_option,
|
|
8
|
-
:number_of_guests, :
|
|
8
|
+
:number_of_guests, :discontinue_on, :high_demand
|
|
9
9
|
|
|
10
10
|
base.attribute :delivery_required, &:delivery_required?
|
|
11
|
+
base.attribute(:max_quantity_per_order) { |variant| variant.product&.max_quantity_per_order }
|
|
11
12
|
|
|
12
13
|
base.has_many :stock_locations
|
|
13
14
|
base.has_many :stock_items, serializer: ::SpreeCmCommissioner::V2::Storefront::StockItemSerializer
|
|
@@ -8,6 +8,11 @@ module Spree
|
|
|
8
8
|
|
|
9
9
|
# Overrided dynamic fields relationship of the parent serializer(Storefront).
|
|
10
10
|
has_many :dynamic_fields, serializer: Spree::V2::Tenant::DynamicFieldSerializer
|
|
11
|
+
|
|
12
|
+
attribute :max_quantity_per_order, &:max_quantity_per_order
|
|
13
|
+
attribute :max_quantity_per_account, &:max_quantity_per_account
|
|
14
|
+
attribute :max_quantity_per_device, &:max_quantity_per_device
|
|
15
|
+
attribute :max_quantity_per_ip, &:max_quantity_per_ip
|
|
11
16
|
end
|
|
12
17
|
end
|
|
13
18
|
end
|
|
@@ -30,9 +30,10 @@ module Spree
|
|
|
30
30
|
|
|
31
31
|
attributes :need_confirmation, :product_type, :kyc, :allow_anonymous_booking,
|
|
32
32
|
:reminder_in_hours, :start_time, :delivery_option,
|
|
33
|
-
:number_of_guests, :
|
|
33
|
+
:number_of_guests, :discontinue_on, :high_demand
|
|
34
34
|
|
|
35
35
|
attribute :delivery_required, &:delivery_required?
|
|
36
|
+
attribute(:max_quantity_per_order) { |variant| variant.product&.max_quantity_per_order }
|
|
36
37
|
|
|
37
38
|
belongs_to :product, serializer: Spree::V2::Storefront::ProductSerializer
|
|
38
39
|
|
|
@@ -27,19 +27,28 @@ module SpreeCmCommissioner
|
|
|
27
27
|
class Create
|
|
28
28
|
prepend ::Spree::ServiceModule::Base
|
|
29
29
|
|
|
30
|
-
def call(line_items:, user: nil, store: nil, currency: nil, tenant: nil)
|
|
30
|
+
def call(line_items:, user: nil, store: nil, currency: nil, tenant: nil, options: {}) # rubocop:disable Metrics/ParameterLists
|
|
31
31
|
return failure(nil, I18n.t('spree_cm_commissioner.draft_order.line_items_missing')) if line_items.blank?
|
|
32
32
|
|
|
33
|
-
order = create_order!(line_items, user, store, currency, tenant)
|
|
33
|
+
order = create_order!(line_items, user, store, currency, tenant, options)
|
|
34
34
|
success(order: order)
|
|
35
35
|
rescue StandardError => e
|
|
36
36
|
failure(nil, e.message)
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
def create_order!(line_items_params, user, store, currency, tenant)
|
|
39
|
+
def create_order!(line_items_params, user, store, currency, tenant, options) # rubocop:disable Metrics/ParameterLists
|
|
40
40
|
store ||= Spree::Store.default
|
|
41
41
|
currency ||= store&.default_currency
|
|
42
|
-
order = Spree::Order.new(
|
|
42
|
+
order = Spree::Order.new(
|
|
43
|
+
state: 'cart',
|
|
44
|
+
use_billing: true,
|
|
45
|
+
user: user,
|
|
46
|
+
store: store,
|
|
47
|
+
currency: currency,
|
|
48
|
+
tenant: tenant,
|
|
49
|
+
device_id: options[:device_id],
|
|
50
|
+
last_ip_address: options[:last_ip_address]
|
|
51
|
+
)
|
|
43
52
|
line_items_params.each { |line_item_params| build_line_item!(order, store, line_item_params) }
|
|
44
53
|
|
|
45
54
|
ActiveRecord::Base.transaction do
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Per-product purchase caps, one independent check per scope. The order cap looks at a single
|
|
2
|
+
# order; account/ip/device aggregate over the buyer's completed orders, so a customer can't
|
|
3
|
+
# bypass a cap by splitting the purchase across carts.
|
|
4
|
+
#
|
|
5
|
+
# A cap of nil or 0 is off. A scope with no value on the order (guest order, no IP, no device
|
|
6
|
+
# id) has nothing to count the buyer against, so its cap does not apply.
|
|
7
|
+
#
|
|
8
|
+
# Counting is a live aggregation over spree_orders/spree_line_items - no ledger table,
|
|
9
|
+
# nothing to keep in sync, always reflects current order state.
|
|
10
|
+
module SpreeCmCommissioner
|
|
11
|
+
module LineItems
|
|
12
|
+
class ValidatePurchaseLimits
|
|
13
|
+
prepend ::Spree::ServiceModule::Base
|
|
14
|
+
|
|
15
|
+
LimitExceededError = Class.new(StandardError)
|
|
16
|
+
|
|
17
|
+
COUNTED_STATES = %w[complete resumed].freeze
|
|
18
|
+
|
|
19
|
+
def call(line_item:)
|
|
20
|
+
@line_item = line_item
|
|
21
|
+
@product = line_item.product
|
|
22
|
+
@order = line_item.order
|
|
23
|
+
|
|
24
|
+
return success(nil) if @product.blank?
|
|
25
|
+
|
|
26
|
+
check_order_limit!
|
|
27
|
+
check_account_limit!
|
|
28
|
+
check_ip_limit!
|
|
29
|
+
check_device_limit!
|
|
30
|
+
|
|
31
|
+
success(nil)
|
|
32
|
+
rescue LimitExceededError => e
|
|
33
|
+
failure(nil, e.message)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def check_order_limit!
|
|
39
|
+
limit = @product.max_quantity_per_order.to_i
|
|
40
|
+
return unless limit.positive?
|
|
41
|
+
return if quantity_in_order <= limit
|
|
42
|
+
|
|
43
|
+
raise LimitExceededError, I18n.t('line_item.validation.exceeded_order_purchase_limit')
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def check_account_limit!
|
|
47
|
+
limit = @product.max_quantity_per_account.to_i
|
|
48
|
+
return unless limit.positive? && @order&.user_id.present?
|
|
49
|
+
return if quantity_in_order + purchased_by(user_id: @order.user_id) <= limit
|
|
50
|
+
|
|
51
|
+
raise LimitExceededError, I18n.t('line_item.validation.exceeded_account_purchase_limit')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def check_ip_limit!
|
|
55
|
+
limit = @product.max_quantity_per_ip.to_i
|
|
56
|
+
return unless limit.positive? && @order&.last_ip_address.present?
|
|
57
|
+
return if quantity_in_order + purchased_by(last_ip_address: @order.last_ip_address) <= limit
|
|
58
|
+
|
|
59
|
+
raise LimitExceededError, I18n.t('line_item.validation.exceeded_ip_purchase_limit')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def check_device_limit!
|
|
63
|
+
limit = @product.max_quantity_per_device.to_i
|
|
64
|
+
return unless limit.positive? && @order&.device_id.present?
|
|
65
|
+
return if quantity_in_order + purchased_by(device_id: @order.device_id) <= limit
|
|
66
|
+
|
|
67
|
+
raise LimitExceededError, I18n.t('line_item.validation.exceeded_device_purchase_limit')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Sums per product, not per line item: two variants of one product share one allowance,
|
|
71
|
+
# so 3 + 3 must not both pass a cap of 4.
|
|
72
|
+
def quantity_in_order
|
|
73
|
+
@quantity_in_order ||= begin
|
|
74
|
+
siblings = @order ? @order.line_items.reject { |item| item == @line_item } : []
|
|
75
|
+
# One variant load for the whole cart rather than one per sibling; already-loaded
|
|
76
|
+
# siblings are skipped.
|
|
77
|
+
ActiveRecord::Associations::Preloader.new(records: siblings, associations: :variant).call if siblings.any?
|
|
78
|
+
|
|
79
|
+
@line_item.quantity.to_i + siblings.sum { |item| item.variant&.product_id == @product.id ? item.quantity : 0 }
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# What the buyer already holds for this product, outside the order being validated.
|
|
84
|
+
# product_id is not a column on spree_line_items - it is `delegate :product_id, to:
|
|
85
|
+
# :variant` - so the product filter joins through spree_variants, indexed on product_id.
|
|
86
|
+
def purchased_by(scope)
|
|
87
|
+
Spree::LineItem.joins(:order, :variant)
|
|
88
|
+
.where(spree_orders: { state: COUNTED_STATES })
|
|
89
|
+
.where(spree_orders: scope)
|
|
90
|
+
.where(spree_variants: { product_id: @product.id })
|
|
91
|
+
.where.not(spree_orders: { completed_at: nil })
|
|
92
|
+
.where.not(spree_orders: { id: @line_item.order_id })
|
|
93
|
+
.sum(:quantity)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -117,7 +117,7 @@ module SpreeCmCommissioner
|
|
|
117
117
|
request_to_book_issue(variant),
|
|
118
118
|
out_of_stock_issue(variant),
|
|
119
119
|
seat_selection_issue(variant, product),
|
|
120
|
-
max_quantity_issue(
|
|
120
|
+
max_quantity_issue(product),
|
|
121
121
|
missing_event_issue(event)
|
|
122
122
|
].compact
|
|
123
123
|
end
|
|
@@ -176,11 +176,11 @@ module SpreeCmCommissioner
|
|
|
176
176
|
'Variant requires seat selection (it has seat blocks).'
|
|
177
177
|
end
|
|
178
178
|
|
|
179
|
-
def max_quantity_issue(
|
|
180
|
-
max =
|
|
179
|
+
def max_quantity_issue(product)
|
|
180
|
+
max = product.max_quantity_per_order
|
|
181
181
|
return if max.blank? || max >= 1
|
|
182
182
|
|
|
183
|
-
"
|
|
183
|
+
"Product max_quantity_per_order is #{max} (less than one ticket)."
|
|
184
184
|
end
|
|
185
185
|
|
|
186
186
|
def missing_event_issue(event)
|
data/config/locales/en.yml
CHANGED
|
@@ -163,7 +163,10 @@ en:
|
|
|
163
163
|
|
|
164
164
|
line_item:
|
|
165
165
|
validation:
|
|
166
|
-
|
|
166
|
+
exceeded_order_purchase_limit: "You have reached the maximum quantity allowed per order for this item."
|
|
167
|
+
exceeded_account_purchase_limit: "You have reached the purchase limit for this item."
|
|
168
|
+
exceeded_ip_purchase_limit: "This network has reached the purchase limit for this item."
|
|
169
|
+
exceeded_device_purchase_limit: "This device has reached the purchase limit for this item."
|
|
167
170
|
seats_are_required: "Seats are required for all guests"
|
|
168
171
|
blocks_are_reserved_by_other_guest: "Seats were recently reserved by another guest"
|
|
169
172
|
blocks_are_reserved_by_other_guest_with_label: "Seat %{label} was recently reserved by another guest"
|
|
@@ -173,6 +176,14 @@ en:
|
|
|
173
176
|
unable_to_save_reserved_block_record: "Unable to save seat reservation"
|
|
174
177
|
unable_to_save_reserved_block_record_with_label: "Unable to save reservation for seat %{label}"
|
|
175
178
|
|
|
179
|
+
errors:
|
|
180
|
+
messages:
|
|
181
|
+
must_be_positive_integer: "must be a positive whole number"
|
|
182
|
+
|
|
183
|
+
purchase_limit:
|
|
184
|
+
device_cap_requires_app_only: "Device limit requires the ticket to be app-only."
|
|
185
|
+
account_cap_requires_named_booking: "Account limit requires anonymous booking to be off."
|
|
186
|
+
|
|
176
187
|
vectors:
|
|
177
188
|
icons:
|
|
178
189
|
info_rules: 'Only allow <b>%{allow_extensions}</b> files. Icons are stored inside "images/" folder, eg. app/assets/images/backend-adjust.svg.'
|
data/config/locales/km.yml
CHANGED
|
@@ -133,7 +133,10 @@ km:
|
|
|
133
133
|
|
|
134
134
|
line_item:
|
|
135
135
|
validation:
|
|
136
|
-
|
|
136
|
+
exceeded_order_purchase_limit: "អ្នកបានឈានដល់ចំនួនអតិបរមាក្នុងមួយការបញ្ជាទិញសម្រាប់ទំនិញនេះ។"
|
|
137
|
+
exceeded_account_purchase_limit: "អ្នកបានឈានដល់ដែនកំណត់នៃការទិញសម្រាប់ទំនិញនេះ។"
|
|
138
|
+
exceeded_ip_purchase_limit: "បណ្តាញនេះបានឈានដល់ដែនកំណត់នៃការទិញសម្រាប់ទំនិញនេះ។"
|
|
139
|
+
exceeded_device_purchase_limit: "ឧបករណ៍នេះបានឈានដល់ដែនកំណត់នៃការទិញសម្រាប់ទំនិញនេះ។"
|
|
137
140
|
seats_are_required: "ត្រូវការលេខកៅអីសម្រាប់ភ្ញៀវទាំងអស់"
|
|
138
141
|
blocks_are_reserved_by_other_guest: "កៅអីត្រូវបានកក់ថ្មីៗដោយភ្ញៀវផ្សេងទៀត"
|
|
139
142
|
blocks_are_reserved_by_other_guest_with_label: "កៅអី %{label} ត្រូវបានកក់ថ្មីៗដោយភ្ញៀវផ្សេងទៀត"
|
|
@@ -143,6 +146,14 @@ km:
|
|
|
143
146
|
unable_to_save_reserved_block_record: "មិនអាចរក្សាទុកការកក់កៅអីបានទេ"
|
|
144
147
|
unable_to_save_reserved_block_record_with_label: "មិនអាចរក្សាទុកការកក់សម្រាប់កៅអី %{label} បានទេ"
|
|
145
148
|
|
|
149
|
+
errors:
|
|
150
|
+
messages:
|
|
151
|
+
must_be_positive_integer: "ត្រូវតែជាចំនួនគត់វិជ្ជមាន"
|
|
152
|
+
|
|
153
|
+
purchase_limit:
|
|
154
|
+
device_cap_requires_app_only: "ដែនកំណត់តាមឧបករណ៍ត្រូវការឱ្យសំបុត្រមានតែក្នុងកម្មវិធីប៉ុណ្ណោះ។"
|
|
155
|
+
account_cap_requires_named_booking: "ដែនកំណត់តាមគណនីត្រូវការឱ្យបិទការកក់ដោយអនាមិក។"
|
|
156
|
+
|
|
146
157
|
subscription:
|
|
147
158
|
validation:
|
|
148
159
|
out_of_range: "Subscription date is out of range."
|
|
@@ -436,11 +447,11 @@ km:
|
|
|
436
447
|
attributes:
|
|
437
448
|
base:
|
|
438
449
|
last_contestant_in_parent_session: 'មិនអាចដកអ្នកចូលប្រកួតចុងក្រោយពី "%{session_name}" ព្រោះ session កូនពឹងផ្អែកលើវា'
|
|
439
|
-
advanced_from_session:
|
|
440
|
-
pre_vote_session:
|
|
441
|
-
has_advanced_to_session:
|
|
450
|
+
advanced_from_session: "ត្រូវបានដំឡើងពី session មុន ហើយមិនអាចដកចេញដោយផ្ទាល់បានទេ។ សូមលុបការដំឡើងនៅក្នុង session ប្រភពជំនួសវិញ។"
|
|
451
|
+
pre_vote_session: "មិនអាចដកចេញពី pre-vote session បានទេ។"
|
|
452
|
+
has_advanced_to_session: "ត្រូវបានកំណត់ឱ្យដំឡើងទៅ session មួយទៀត។ សូមលុបការដំឡើងជាមុនសិន មុននឹងដកអ្នកចូលប្រកួតនេះចេញ។"
|
|
442
453
|
advanced_to_id:
|
|
443
|
-
same_as_advanced_from:
|
|
454
|
+
same_as_advanced_from: "មិនអាចដូចនឹងអ្នកចូលប្រកួតដែលវាត្រូវបានដំឡើងចេញពីនោះទេ"
|
|
444
455
|
spree_cm_commissioner/voting_credit:
|
|
445
456
|
attributes:
|
|
446
457
|
free_votes_amount:
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class AddPurchaseLimitScopesToSpreeOrders < ActiveRecord::Migration[7.0]
|
|
2
|
+
# Partial, matching ValidatePurchaseLimits::COUNTED_STATES. Most spree_orders rows are
|
|
3
|
+
# abandoned carts the purchase-limit query never looks at, so indexing them only costs
|
|
4
|
+
# storage. Keeping state in the predicate also drops a varchar from every entry.
|
|
5
|
+
COUNTED = "state IN ('complete', 'resumed')".freeze
|
|
6
|
+
|
|
7
|
+
def up
|
|
8
|
+
add_column :spree_orders, :device_id, :string, if_not_exists: true
|
|
9
|
+
|
|
10
|
+
add_index :spree_orders, :user_id, where: COUNTED, name: 'idx_orders_user_for_limits', if_not_exists: true
|
|
11
|
+
add_index :spree_orders, :last_ip_address, where: COUNTED, name: 'idx_orders_ip_for_limits', if_not_exists: true
|
|
12
|
+
add_index :spree_orders, :device_id, where: COUNTED, name: 'idx_orders_device_for_limits', if_not_exists: true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def down
|
|
16
|
+
remove_index :spree_orders, name: 'idx_orders_user_for_limits', if_exists: true
|
|
17
|
+
remove_index :spree_orders, name: 'idx_orders_ip_for_limits', if_exists: true
|
|
18
|
+
remove_index :spree_orders, name: 'idx_orders_device_for_limits', if_exists: true
|
|
19
|
+
|
|
20
|
+
remove_column :spree_orders, :device_id, if_exists: true
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -75,12 +75,6 @@ FactoryBot.define do
|
|
|
75
75
|
presentation { 'delivery-option' }
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
trait :max_quantity_per_order do
|
|
79
|
-
attr_type { :integer }
|
|
80
|
-
name { 'max-quantity-per-order' }
|
|
81
|
-
presentation { 'max-quantity-per-order' }
|
|
82
|
-
end
|
|
83
|
-
|
|
84
78
|
trait :due_date do
|
|
85
79
|
attr_type { :integer }
|
|
86
80
|
name { 'due-date' }
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_cm_commissioner
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.9.
|
|
4
|
+
version: 2.9.8
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- You
|
|
@@ -1482,6 +1482,7 @@ files:
|
|
|
1482
1482
|
- app/models/concerns/spree_cm_commissioner/product_delegation.rb
|
|
1483
1483
|
- app/models/concerns/spree_cm_commissioner/product_relation_type.rb
|
|
1484
1484
|
- app/models/concerns/spree_cm_commissioner/product_type.rb
|
|
1485
|
+
- app/models/concerns/spree_cm_commissioner/purchase_limitable.rb
|
|
1485
1486
|
- app/models/concerns/spree_cm_commissioner/route_order_countable.rb
|
|
1486
1487
|
- app/models/concerns/spree_cm_commissioner/route_type.rb
|
|
1487
1488
|
- app/models/concerns/spree_cm_commissioner/service_calendar_type.rb
|
|
@@ -2282,6 +2283,7 @@ files:
|
|
|
2282
2283
|
- app/services/spree_cm_commissioner/invite_guests/bulk_create.rb
|
|
2283
2284
|
- app/services/spree_cm_commissioner/invite_guests/create.rb
|
|
2284
2285
|
- app/services/spree_cm_commissioner/line_items/sync_event_date.rb
|
|
2286
|
+
- app/services/spree_cm_commissioner/line_items/validate_purchase_limits.rb
|
|
2285
2287
|
- app/services/spree_cm_commissioner/load_test/create_accounts.rb
|
|
2286
2288
|
- app/services/spree_cm_commissioner/load_test/destroy_accounts.rb
|
|
2287
2289
|
- app/services/spree_cm_commissioner/load_test/flow_registry.rb
|
|
@@ -3463,6 +3465,7 @@ files:
|
|
|
3463
3465
|
- db/migrate/20260728074153_add_role_to_cm_exports.rb
|
|
3464
3466
|
- db/migrate/20260729000000_add_visibility_to_cm_trips.rb
|
|
3465
3467
|
- db/migrate/20260729000001_add_session_created_at_index_to_cm_votes.rb
|
|
3468
|
+
- db/migrate/20260730000000_add_purchase_limit_scopes_to_spree_orders.rb
|
|
3466
3469
|
- docker-compose.yml
|
|
3467
3470
|
- docs/api/scoped-access-token-endpoints.md
|
|
3468
3471
|
- docs/option_types/attr_types.md
|