spree_cm_commissioner 2.9.7 → 2.9.9
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/content_cachable.rb +1 -0
- 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/storefront/payment_methods_controller.rb +40 -0
- data/app/controllers/spree/api/v2/tenant/draft_orders_controller.rb +2 -1
- data/app/controllers/spree/api/v2/tenant/payment_methods_controller.rb +54 -0
- data/app/finders/spree_cm_commissioner/payment_methods/find.rb +59 -0
- 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/user_serializer_decorator.rb +2 -1
- 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/user_serializer.rb +1 -1
- data/app/serializers/spree/v2/tenant/variant_serializer.rb +2 -1
- data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +24 -3
- 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/config/routes.rb +2 -0
- 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 +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 391b15d7aada08289da9a21d18a9d48d461ecf1fe0c36a361a276df201455a5e
|
|
4
|
+
data.tar.gz: af3f07ac5d9e449cb5300949dbbcdcb80981b78b41f0aa23a396ce6cdd49e68b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 49e38586ae76dfcf37be8bb6f9c4ddef60a2ef694f491ffb648b5903070dd0932c2efb1d936f3c5a6b4c1af16355b2ddc26e4706cc04c7ed32bed0908ed88db5
|
|
7
|
+
data.tar.gz: be58778bd8a6124b8b3c914e9c0c83a44dce42a75c1df0f677ef00868bc388f3fa88518828e1833b701366eedf551e03a30528919a2c1f8ea16aec108d92b8fe
|
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?
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V2
|
|
4
|
+
module Storefront
|
|
5
|
+
# Order & user independent catalog of the payment methods a vendor accepts.
|
|
6
|
+
# Takes vendor_id as a required query param. See
|
|
7
|
+
# Spree::Api::V2::Tenant::PaymentMethodsController for the caching caveat
|
|
8
|
+
# that comes with that, and for what this deliberately leaves out.
|
|
9
|
+
class PaymentMethodsController < Spree::Api::V2::ResourceController
|
|
10
|
+
# override
|
|
11
|
+
def index
|
|
12
|
+
render_serialized_payload do
|
|
13
|
+
collection_serializer.new(collection).serializable_hash
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# override
|
|
18
|
+
def collection
|
|
19
|
+
@collection ||= SpreeCmCommissioner::PaymentMethods::Find.new(
|
|
20
|
+
vendor: vendor,
|
|
21
|
+
store: current_store,
|
|
22
|
+
type: params[:type]
|
|
23
|
+
).execute
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# override
|
|
27
|
+
def collection_serializer
|
|
28
|
+
::Spree::V2::Storefront::PaymentMethodSerializer
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def vendor
|
|
34
|
+
@vendor ||= ::Spree::Vendor.active.find(params[:vendor_id])
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V2
|
|
4
|
+
module Tenant
|
|
5
|
+
# Order & user independent catalog of the payment methods a vendor accepts.
|
|
6
|
+
#
|
|
7
|
+
# vendor_id is a required query param. A vendor id is globally unique and
|
|
8
|
+
# belongs to exactly one tenant, so it is what identifies tenant, vendor and
|
|
9
|
+
# store here — the tenant itself comes from the Doorkeeper token, not the URL.
|
|
10
|
+
#
|
|
11
|
+
# IMPORTANT for caching (see SpreeCmCommissioner::ContentCachable): the CDN
|
|
12
|
+
# must include vendor_id in its cache key. If query strings are stripped,
|
|
13
|
+
# every vendor collapses into one cache entry and the wrong payment methods
|
|
14
|
+
# are served. Moving to /vendors/:vendor_id/payment_methods would remove that
|
|
15
|
+
# dependency — tracked as a follow-up.
|
|
16
|
+
#
|
|
17
|
+
# For the per-order truth (store credit, early adopter, payout filtering)
|
|
18
|
+
# use CartPaymentMethodGroupsController instead.
|
|
19
|
+
class PaymentMethodsController < BaseController
|
|
20
|
+
# override
|
|
21
|
+
def index
|
|
22
|
+
render_serialized_payload do
|
|
23
|
+
collection_serializer.new(collection).serializable_hash
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# override
|
|
28
|
+
def collection
|
|
29
|
+
@collection ||= SpreeCmCommissioner::PaymentMethods::Find.new(
|
|
30
|
+
vendor: vendor,
|
|
31
|
+
store: current_store,
|
|
32
|
+
type: params[:type]
|
|
33
|
+
).execute
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# override
|
|
37
|
+
def collection_serializer
|
|
38
|
+
Spree::V2::Tenant::PaymentMethodSerializer
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
# Scoped to the token's tenant, so a token from another tenant gets a 404
|
|
44
|
+
# rather than a cache hit on someone else's vendor.
|
|
45
|
+
def vendor
|
|
46
|
+
@vendor ||= MultiTenant.with(@tenant) do
|
|
47
|
+
::Spree::Vendor.active.find(params[:vendor_id])
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module PaymentMethods
|
|
3
|
+
# Order & user independent lookup of the payment methods a vendor accepts.
|
|
4
|
+
#
|
|
5
|
+
# Mirrors the scope selection of Vpago::OrderDecorator#available_payment_methods,
|
|
6
|
+
# minus everything that depends on the order or the current user, so the result
|
|
7
|
+
# is safe to cache on the vendor id alone:
|
|
8
|
+
#
|
|
9
|
+
# - available_for_order? (only Spree::PaymentMethod::StoreCredit varies, on user balance)
|
|
10
|
+
# - available_on_frontend_for_early_adopter (user attribute)
|
|
11
|
+
# - support_payout? (order line items)
|
|
12
|
+
# - preferred_payment_method_id (user, affects sort only)
|
|
13
|
+
#
|
|
14
|
+
# Callers needing the per-order truth must keep using
|
|
15
|
+
# Spree::Api::V2::*::CartPaymentMethodGroupsController.
|
|
16
|
+
class Find
|
|
17
|
+
attr_reader :vendor, :store, :type
|
|
18
|
+
|
|
19
|
+
def initialize(vendor:, store: nil, type: nil)
|
|
20
|
+
@vendor = vendor
|
|
21
|
+
@store = store
|
|
22
|
+
@type = type
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def execute
|
|
26
|
+
return mini_app_scope if type.present?
|
|
27
|
+
|
|
28
|
+
scope.reject(&:mini_app?)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def scope
|
|
34
|
+
@scope ||= source.available_on_front_end.order(:position).to_a
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Vendors without their own payment methods fall back to the store's,
|
|
38
|
+
# same as Vpago::OrderDecorator#available_payment_methods.
|
|
39
|
+
def source
|
|
40
|
+
vendor_payment_methods.exists? ? vendor_payment_methods : store_payment_methods
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def vendor_payment_methods
|
|
44
|
+
@vendor_payment_methods ||= vendor&.payment_methods || Spree::PaymentMethod.none
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def store_payment_methods
|
|
48
|
+
(store || Spree::Store.default).payment_methods
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def mini_app_scope
|
|
52
|
+
mapped_type = SpreeCmCommissioner::PaymentMethodTypeMapper.call(type)
|
|
53
|
+
return [] if mapped_type.blank?
|
|
54
|
+
|
|
55
|
+
scope.select { |payment_method| payment_method.mini_app? && payment_method.type == mapped_type }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -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
|
|
@@ -7,7 +7,8 @@ module Spree
|
|
|
7
7
|
:country_code, :otp_enabled, :otp_email, :otp_phone_number,
|
|
8
8
|
:confirm_pin_code_enabled, :tenant_id,
|
|
9
9
|
:login, :qr_data, :qr_data_version, :qr_data_invalidated_at,
|
|
10
|
-
:unread_notifications_count, :request_orders_count
|
|
10
|
+
:unread_notifications_count, :request_orders_count,
|
|
11
|
+
:preferred_payment_method_id
|
|
11
12
|
|
|
12
13
|
base.attribute :has_incomplete_guest_info, &:has_incomplete_guest_info?
|
|
13
14
|
|
|
@@ -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
|
|
@@ -6,7 +6,7 @@ module Spree
|
|
|
6
6
|
:public_metadata, :completed_orders,
|
|
7
7
|
:phone_number, :intel_phone_number, :country_code,
|
|
8
8
|
:otp_enabled, :otp_email, :otp_phone_number,
|
|
9
|
-
:confirm_pin_code_enabled
|
|
9
|
+
:confirm_pin_code_enabled, :preferred_payment_method_id
|
|
10
10
|
|
|
11
11
|
has_one :profile, serializer: Spree::V2::Tenant::AssetSerializer
|
|
12
12
|
has_many :device_tokens, serializer: Spree::V2::Tenant::UserDeviceTokenSerializer
|
|
@@ -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
|
|
|
@@ -9,7 +9,8 @@ module SpreeCmCommissioner
|
|
|
9
9
|
'Spree::Menu' => Spree::Menu,
|
|
10
10
|
'Spree::Taxon' => Spree::Taxon,
|
|
11
11
|
'Spree::Product' => Spree::Product,
|
|
12
|
-
'Spree::Vendor' => Spree::Vendor
|
|
12
|
+
'Spree::Vendor' => Spree::Vendor,
|
|
13
|
+
'Spree::PaymentMethod' => Spree::PaymentMethod
|
|
13
14
|
}.freeze
|
|
14
15
|
|
|
15
16
|
# eg. 'Spree::Product', 123 or 'Spree::Product', nil
|
|
@@ -84,7 +85,8 @@ module SpreeCmCommissioner
|
|
|
84
85
|
'/api/v2/storefront/vendors/:id*',
|
|
85
86
|
'/api/v2/storefront/accommodations/:id*',
|
|
86
87
|
'/api/v2/storefront/accommodations/:slug*'
|
|
87
|
-
]
|
|
88
|
+
],
|
|
89
|
+
'Spree::PaymentMethod' => payment_method_patterns
|
|
88
90
|
}
|
|
89
91
|
end
|
|
90
92
|
|
|
@@ -107,12 +109,31 @@ module SpreeCmCommissioner
|
|
|
107
109
|
'/api/v2/tenant/vendors/*',
|
|
108
110
|
'/api/v2/storefront/vendors/*',
|
|
109
111
|
'/api/v2/storefront/accommodations/*'
|
|
110
|
-
]
|
|
112
|
+
],
|
|
113
|
+
'Spree::PaymentMethod' => payment_method_patterns
|
|
111
114
|
}
|
|
112
115
|
end
|
|
113
116
|
|
|
114
117
|
private
|
|
115
118
|
|
|
119
|
+
# Deliberately broad, and identical for single-record and all-record
|
|
120
|
+
# invalidation: a payment method is either vendor-owned or store-level, and a
|
|
121
|
+
# store-level one is served for every vendor through the fallback in
|
|
122
|
+
# SpreeCmCommissioner::PaymentMethods::Find. From the record alone we cannot
|
|
123
|
+
# tell which vendors are affected.
|
|
124
|
+
#
|
|
125
|
+
# vendor_id is a query param, so the trailing `*` is what clears every
|
|
126
|
+
# vendor's cached variant of this path in one go.
|
|
127
|
+
#
|
|
128
|
+
# Payment methods change a handful of times a year, so a broad invalidation
|
|
129
|
+
# is cheap.
|
|
130
|
+
def payment_method_patterns
|
|
131
|
+
[
|
|
132
|
+
'/api/v2/tenant/payment_methods*',
|
|
133
|
+
'/api/v2/storefront/payment_methods*'
|
|
134
|
+
]
|
|
135
|
+
end
|
|
136
|
+
|
|
116
137
|
# eg. updated_at: Fri, 26 Dec 2025 10:40:56.505450000 +07 +07:00
|
|
117
138
|
def refresh_record_cache_key(record)
|
|
118
139
|
# rubocop:disable Rails/SkipsModelValidations
|
|
@@ -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:
|
data/config/routes.rb
CHANGED
|
@@ -532,6 +532,7 @@ Spree::Core::Engine.add_routes do
|
|
|
532
532
|
namespace :tenant do
|
|
533
533
|
resource :token, controller: :access_tokens, only: [:create]
|
|
534
534
|
resources :vendors
|
|
535
|
+
resources :payment_methods, only: %i[index]
|
|
535
536
|
resources :products
|
|
536
537
|
resources :taxons, only: %i[index show], id: /.+/
|
|
537
538
|
resources :ad_campaigns, only: [:index]
|
|
@@ -757,6 +758,7 @@ Spree::Core::Engine.add_routes do
|
|
|
757
758
|
resources :vendor_photos
|
|
758
759
|
resources :events
|
|
759
760
|
end
|
|
761
|
+
resources :payment_methods, only: %i[index]
|
|
760
762
|
resource :homepage_data, only: [:show]
|
|
761
763
|
resources :homepage_sections, only: [:index]
|
|
762
764
|
resources :order_qrs, only: [:show]
|
|
@@ -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.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- You
|
|
@@ -1004,6 +1004,7 @@ files:
|
|
|
1004
1004
|
- app/controllers/spree/api/v2/storefront/order_promotions_controller.rb
|
|
1005
1005
|
- app/controllers/spree/api/v2/storefront/order_qrs_controller.rb
|
|
1006
1006
|
- app/controllers/spree/api/v2/storefront/order_request_notifications_controller.rb
|
|
1007
|
+
- app/controllers/spree/api/v2/storefront/payment_methods_controller.rb
|
|
1007
1008
|
- app/controllers/spree/api/v2/storefront/pending_line_items_controller.rb
|
|
1008
1009
|
- app/controllers/spree/api/v2/storefront/pin_code_checkers_controller.rb
|
|
1009
1010
|
- app/controllers/spree/api/v2/storefront/pin_code_generators_controller.rb
|
|
@@ -1074,6 +1075,7 @@ files:
|
|
|
1074
1075
|
- app/controllers/spree/api/v2/tenant/my_votes_controller.rb
|
|
1075
1076
|
- app/controllers/spree/api/v2/tenant/notifications_controller.rb
|
|
1076
1077
|
- app/controllers/spree/api/v2/tenant/order_histories_controller.rb
|
|
1078
|
+
- app/controllers/spree/api/v2/tenant/payment_methods_controller.rb
|
|
1077
1079
|
- app/controllers/spree/api/v2/tenant/pin_code_checkers_controller.rb
|
|
1078
1080
|
- app/controllers/spree/api/v2/tenant/pin_code_generators_controller.rb
|
|
1079
1081
|
- app/controllers/spree/api/v2/tenant/popular_route_places_controller.rb
|
|
@@ -1201,6 +1203,7 @@ files:
|
|
|
1201
1203
|
- app/finders/spree_cm_commissioner/orders/find.rb
|
|
1202
1204
|
- app/finders/spree_cm_commissioner/orders/find_by_all_state.rb
|
|
1203
1205
|
- app/finders/spree_cm_commissioner/orders/find_by_state.rb
|
|
1206
|
+
- app/finders/spree_cm_commissioner/payment_methods/find.rb
|
|
1204
1207
|
- app/finders/spree_cm_commissioner/payment_methods/group_by_bank.rb
|
|
1205
1208
|
- app/finders/spree_cm_commissioner/places/find_with_route.rb
|
|
1206
1209
|
- app/finders/spree_cm_commissioner/products/find_decorator.rb
|
|
@@ -1482,6 +1485,7 @@ files:
|
|
|
1482
1485
|
- app/models/concerns/spree_cm_commissioner/product_delegation.rb
|
|
1483
1486
|
- app/models/concerns/spree_cm_commissioner/product_relation_type.rb
|
|
1484
1487
|
- app/models/concerns/spree_cm_commissioner/product_type.rb
|
|
1488
|
+
- app/models/concerns/spree_cm_commissioner/purchase_limitable.rb
|
|
1485
1489
|
- app/models/concerns/spree_cm_commissioner/route_order_countable.rb
|
|
1486
1490
|
- app/models/concerns/spree_cm_commissioner/route_type.rb
|
|
1487
1491
|
- app/models/concerns/spree_cm_commissioner/service_calendar_type.rb
|
|
@@ -2282,6 +2286,7 @@ files:
|
|
|
2282
2286
|
- app/services/spree_cm_commissioner/invite_guests/bulk_create.rb
|
|
2283
2287
|
- app/services/spree_cm_commissioner/invite_guests/create.rb
|
|
2284
2288
|
- app/services/spree_cm_commissioner/line_items/sync_event_date.rb
|
|
2289
|
+
- app/services/spree_cm_commissioner/line_items/validate_purchase_limits.rb
|
|
2285
2290
|
- app/services/spree_cm_commissioner/load_test/create_accounts.rb
|
|
2286
2291
|
- app/services/spree_cm_commissioner/load_test/destroy_accounts.rb
|
|
2287
2292
|
- app/services/spree_cm_commissioner/load_test/flow_registry.rb
|
|
@@ -3463,6 +3468,7 @@ files:
|
|
|
3463
3468
|
- db/migrate/20260728074153_add_role_to_cm_exports.rb
|
|
3464
3469
|
- db/migrate/20260729000000_add_visibility_to_cm_trips.rb
|
|
3465
3470
|
- db/migrate/20260729000001_add_session_created_at_index_to_cm_votes.rb
|
|
3471
|
+
- db/migrate/20260730000000_add_purchase_limit_scopes_to_spree_orders.rb
|
|
3466
3472
|
- docker-compose.yml
|
|
3467
3473
|
- docs/api/scoped-access-token-endpoints.md
|
|
3468
3474
|
- docs/option_types/attr_types.md
|