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.
Files changed (25) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/controllers/concerns/spree_cm_commissioner/order_concern.rb +5 -0
  4. data/app/controllers/spree/api/v2/storefront/draft_orders_controller.rb +2 -1
  5. data/app/controllers/spree/api/v2/tenant/draft_orders_controller.rb +2 -1
  6. data/app/interactors/spree_cm_commissioner/create_ticket.rb +7 -3
  7. data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +0 -1
  8. data/app/models/concerns/spree_cm_commissioner/purchase_limitable.rb +59 -0
  9. data/app/models/concerns/spree_cm_commissioner/variant_options_concern.rb +0 -1
  10. data/app/models/spree_cm_commissioner/line_item_decorator.rb +4 -3
  11. data/app/models/spree_cm_commissioner/product_decorator.rb +1 -0
  12. data/app/models/spree_cm_commissioner/variant_options.rb +0 -4
  13. data/app/serializers/spree/v2/storefront/product_serializer_decorator.rb +4 -0
  14. data/app/serializers/spree/v2/storefront/variant_serializer_decorator.rb +2 -1
  15. data/app/serializers/spree/v2/tenant/product_serializer.rb +5 -0
  16. data/app/serializers/spree/v2/tenant/variant_serializer.rb +2 -1
  17. data/app/services/spree_cm_commissioner/draft_order/create.rb +13 -4
  18. data/app/services/spree_cm_commissioner/line_items/validate_purchase_limits.rb +97 -0
  19. data/app/services/spree_cm_commissioner/load_test/generate_booking_ticket_data.rb +4 -4
  20. data/config/locales/en.yml +12 -1
  21. data/config/locales/km.yml +16 -5
  22. data/db/migrate/20260730000000_add_purchase_limit_scopes_to_spree_orders.rb +22 -0
  23. data/lib/spree_cm_commissioner/test_helper/factories/option_type_factory.rb +0 -6
  24. data/lib/spree_cm_commissioner/version.rb +1 -1
  25. metadata +4 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e267212ae81075d66a95e65c2eb320ea05bc9b18f53b1bc4a74896639ddd4ed2
4
- data.tar.gz: cc28b8d31dd89f481864b6f5c366d264335403ca4f8aaf9a00dbd39e502012b6
3
+ metadata.gz: 8eb692e530b34906dfca4d757e6382b9b643e18760fd38ba31507ceb24bb687b
4
+ data.tar.gz: 782a651003a7817fb539def0a19697d434978c51f7f48c0f2184b48fe87c1260
5
5
  SHA512:
6
- metadata.gz: 7cb85b5d8aaac69dc845ddeb068ec96200cf400a7c6d9f3bd979681fe4d03dd08cf4358a059855825785d9524e1a4d04e0447e3b0bfc682f4827c1b09734fa7d
7
- data.tar.gz: 5727d1541df27c51c58ef2b4a7e022096c62aa6666fcb58514dbe39621a44395478d1f5ea5ecef24280767c0d41f47e43f823276720ce63f45c7ef371278b0a8
6
+ metadata.gz: '090263c66bf5f79ea0edfcf6602185b0d80120f33308b2d0193cacf3bb2ebc5036543f1e1000b4e823a243a31cd3dc2eaf3e4b8772c6717d16891554b3da96a0'
7
+ data.tar.gz: 3540311c234819a354e154a532b7745e91ebf1faf7b15bcc40a5a6def80ea069d3820759b328decb2328f6ad60190a12686d5c23c0951c864ee89215160fd4a1
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.9.7)
37
+ spree_cm_commissioner (2.9.8)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -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?
@@ -13,7 +13,8 @@ module Spree
13
13
  user: spree_current_user,
14
14
  store: current_store,
15
15
  currency: current_currency,
16
- tenant: @tenant
16
+ tenant: @tenant,
17
+ options: { device_id: params[:device_id], last_ip_address: request.remote_ip }
17
18
  )
18
19
 
19
20
  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
- max_order_quantity: context.params[:max_order_quantity],
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
@@ -14,7 +14,6 @@ module SpreeCmCommissioner
14
14
  :duration_in_seconds,
15
15
  :payment_option,
16
16
  :delivery_option,
17
- :max_quantity_per_order,
18
17
  :due_date,
19
18
  :month,
20
19
  :number_of_adults,
@@ -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 :ensure_not_exceed_max_quantity_per_order, if: -> { variant&.max_quantity_per_order.present? }
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 ensure_not_exceed_max_quantity_per_order
294
- errors.add(:quantity, I18n.t('line_item.validation.exceeded_max_quantity_per_order')) if quantity > variant.max_quantity_per_order
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, :max_quantity_per_order, :discontinue_on, :high_demand
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, :max_quantity_per_order, :discontinue_on, :high_demand
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(state: 'cart', use_billing: true, user: user, store: store, currency: currency, tenant: tenant)
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(variant),
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(variant)
180
- max = variant.max_quantity_per_order
179
+ def max_quantity_issue(product)
180
+ max = product.max_quantity_per_order
181
181
  return if max.blank? || max >= 1
182
182
 
183
- "Variant max-quantity-per-order is #{max} (less than one ticket)."
183
+ "Product max_quantity_per_order is #{max} (less than one ticket)."
184
184
  end
185
185
 
186
186
  def missing_event_issue(event)
@@ -163,7 +163,10 @@ en:
163
163
 
164
164
  line_item:
165
165
  validation:
166
- exceeded_max_quantity_per_order: "Exceeded maximum quantity per order"
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.'
@@ -133,7 +133,10 @@ km:
133
133
 
134
134
  line_item:
135
135
  validation:
136
- exceeded_max_quantity_per_order: "លើសពីបរិមាណអតិបរមាក្នុងមួយការបញ្ជាទិញ"
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: 'ត្រូវបានដំឡើងពី session មុន ហើយមិនអាចដកចេញដោយផ្ទាល់បានទេ។ សូមលុបការដំឡើងនៅក្នុង session ប្រភពជំនួសវិញ។'
440
- pre_vote_session: 'មិនអាចដកចេញពី pre-vote session បានទេ។'
441
- has_advanced_to_session: 'ត្រូវបានកំណត់ឱ្យដំឡើងទៅ 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' }
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- VERSION = '2.9.7'.freeze
2
+ VERSION = '2.9.8'.freeze
3
3
 
4
4
  module_function
5
5
 
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.7
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