spree_cm_commissioner 2.9.6 → 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/.env.example +3 -8
- data/Gemfile.lock +1 -1
- data/app/controllers/concerns/spree_cm_commissioner/order_concern.rb +5 -0
- data/app/controllers/spree/admin/users_controller_decorator.rb +18 -0
- data/app/controllers/spree/api/v2/storefront/draft_orders_controller.rb +2 -1
- data/app/controllers/spree/api/v2/storefront/telegram_oauth_tokens_controller.rb +35 -2
- data/app/controllers/spree/api/v2/tenant/draft_orders_controller.rb +2 -1
- data/app/controllers/spree/api/v2/tenant/order_histories_controller.rb +2 -0
- data/app/finders/spree_cm_commissioner/routes/find.rb +1 -1
- data/app/interactors/spree_cm_commissioner/create_ticket.rb +7 -3
- data/app/interactors/spree_cm_commissioner/telegram_oauth_id_token_provider.rb +3 -9
- data/app/interactors/spree_cm_commissioner/telegram_oauth_token_exchanger.rb +5 -5
- data/app/interactors/spree_cm_commissioner/user_telegram_oauth_authenticator.rb +11 -1
- data/app/mailers/spree/order_mailer_decorator.rb +1 -0
- data/app/models/concerns/spree_cm_commissioner/event_metadata.rb +12 -2
- data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +0 -1
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +2 -0
- data/app/models/concerns/spree_cm_commissioner/purchase_limitable.rb +59 -0
- data/app/models/concerns/spree_cm_commissioner/store_preference.rb +2 -0
- data/app/models/concerns/spree_cm_commissioner/tenant_preference.rb +2 -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/seat_layout.rb +12 -4
- data/app/models/spree_cm_commissioner/show.rb +3 -1
- data/app/models/spree_cm_commissioner/show_episode.rb +3 -1
- data/app/models/spree_cm_commissioner/trip.rb +5 -0
- data/app/models/spree_cm_commissioner/variant_options.rb +0 -4
- data/app/models/spree_cm_commissioner/vehicle_type.rb +1 -0
- data/app/overrides/spree/admin/stores/_form/telegram_auth.html.erb.deface +73 -0
- data/app/queries/spree_cm_commissioner/multi_leg_trips_query.rb +5 -0
- data/app/queries/spree_cm_commissioner/single_leg_trips_query.rb +2 -1
- data/app/serializers/spree/v2/storefront/product_serializer_decorator.rb +4 -0
- data/app/serializers/spree/v2/storefront/taxon_serializer_decorator.rb +1 -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/app/services/spree_cm_commissioner/voting_leaderboards/time_range_result.rb +79 -0
- data/app/views/spree/admin/tenants/_form.html.erb +27 -0
- data/app/views/spree/admin/tenants/form/_telegram_auth.html.erb +61 -0
- data/config/initializers/spree_permitted_attributes.rb +2 -0
- data/config/locales/en.yml +14 -1
- data/config/locales/km.yml +19 -5
- data/db/migrate/20260729000000_add_visibility_to_cm_trips.rb +6 -0
- data/db/migrate/20260729000001_add_session_created_at_index_to_cm_votes.rb +12 -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 +10 -2
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/.env.example
CHANGED
|
@@ -32,14 +32,9 @@ WAITING_ROOM_MAX_ETA_BATCH_SIZE=50 # cap on batch size assumed when estimating t
|
|
|
32
32
|
VATTANAC_AES_SECRET_KEY= ""
|
|
33
33
|
VATTANAC_PUBLIC_KEY=""
|
|
34
34
|
|
|
35
|
-
# Telegram OAuth (OpenID Connect) login
|
|
36
|
-
#
|
|
37
|
-
# Telegram
|
|
38
|
-
# app/interactors/spree_cm_commissioner/telegram_oauth_id_token_provider.rb
|
|
39
|
-
TELEGRAM_OAUTH_CLIENT_ID=""
|
|
40
|
-
# client_secret injected server-side when proxying the PKCE code -> id_token
|
|
41
|
-
# exchange in app/interactors/spree_cm_commissioner/telegram_oauth_token_exchanger.rb
|
|
42
|
-
TELEGRAM_OAUTH_CLIENT_SECRET=""
|
|
35
|
+
# Telegram OAuth (OpenID Connect) login client_id/client_secret are configured
|
|
36
|
+
# per tenant (or on the default store for tenant-less logins) from the admin
|
|
37
|
+
# under Telegram Authentication - see docs/authentication/telegram-login.md.
|
|
43
38
|
|
|
44
39
|
# Organizer URL
|
|
45
40
|
ORGANIZER_URL=http://127.0.0.1:4000/organizer
|
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
|
|
@@ -20,8 +20,26 @@ module Spree
|
|
|
20
20
|
end
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# override
|
|
24
|
+
def update
|
|
25
|
+
preserve_non_form_roles
|
|
26
|
+
super
|
|
27
|
+
end
|
|
28
|
+
|
|
23
29
|
private
|
|
24
30
|
|
|
31
|
+
# Role types the user form doesn't render (assigned in organizer dashboards), e.g. check-in groups and preview.
|
|
32
|
+
NON_FORM_ROLE_TYPES = %i[check_in preview].freeze
|
|
33
|
+
|
|
34
|
+
# spree_role_ids= replaces the whole role set, so a submit that omits these would drop them. Merge
|
|
35
|
+
# the user's existing ones back in so they survive. Skip when the form didn't submit roles at all.
|
|
36
|
+
def preserve_non_form_roles
|
|
37
|
+
return if params.dig(:user, :spree_role_ids).nil?
|
|
38
|
+
|
|
39
|
+
preserved_ids = @user.spree_roles.where(role_type: NON_FORM_ROLE_TYPES).pluck(:id).map(&:to_s)
|
|
40
|
+
params[:user][:spree_role_ids] = (params[:user][:spree_role_ids] + preserved_ids).uniq
|
|
41
|
+
end
|
|
42
|
+
|
|
25
43
|
def build_profile
|
|
26
44
|
return unless permitted_resource_params[:profile]
|
|
27
45
|
|
|
@@ -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?
|
|
@@ -9,7 +9,11 @@ module Spree
|
|
|
9
9
|
# has no access token yet (this is part of logging in).
|
|
10
10
|
class TelegramOauthTokensController < Spree::Api::V2::BaseController
|
|
11
11
|
def create
|
|
12
|
-
|
|
12
|
+
return render_error_payload(credential_mismatch_message) if credential_mismatch_message
|
|
13
|
+
|
|
14
|
+
result = SpreeCmCommissioner::TelegramOauthTokenExchanger.call(
|
|
15
|
+
exchange_params.merge(client_secret: resolved_client_secret)
|
|
16
|
+
)
|
|
13
17
|
|
|
14
18
|
return render_error_payload(result.message) unless result.success?
|
|
15
19
|
|
|
@@ -19,7 +23,36 @@ module Spree
|
|
|
19
23
|
private
|
|
20
24
|
|
|
21
25
|
def exchange_params
|
|
22
|
-
params.permit(:client_id, :code, :redirect_uri, :code_verifier)
|
|
26
|
+
params.permit(:client_id, :code, :redirect_uri, :code_verifier).to_h.symbolize_keys
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# `tenant_id` scopes which tenant's bot this login belongs to; with none
|
|
30
|
+
# given, the default store's bot is used. Distinguished from
|
|
31
|
+
# telegram_oauth_client_secret_not_configured (raised by the exchanger
|
|
32
|
+
# when the owner has no secret set) so a caller can tell "wrong
|
|
33
|
+
# tenant/client_id" apart from "correct tenant, nothing configured yet".
|
|
34
|
+
def credential_mismatch_message
|
|
35
|
+
return 'telegram_oauth_tenant_not_found' if params[:tenant_id].present? && credential_owner.nil?
|
|
36
|
+
return nil if credential_owner.nil? || credential_owner.preferred_telegram_oauth_client_id.blank?
|
|
37
|
+
return 'telegram_oauth_client_id_mismatch' unless client_id_matches_owner?
|
|
38
|
+
|
|
39
|
+
nil
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def client_id_matches_owner?
|
|
43
|
+
credential_owner.preferred_telegram_oauth_client_id == exchange_params[:client_id]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def resolved_client_secret
|
|
47
|
+
credential_owner&.preferred_telegram_oauth_client_secret.presence
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def credential_owner
|
|
51
|
+
@credential_owner ||= if params[:tenant_id].present?
|
|
52
|
+
SpreeCmCommissioner::Tenant.find_by(id: params[:tenant_id])
|
|
53
|
+
else
|
|
54
|
+
Spree::Store.default
|
|
55
|
+
end
|
|
23
56
|
end
|
|
24
57
|
end
|
|
25
58
|
end
|
|
@@ -43,6 +43,7 @@ module Spree
|
|
|
43
43
|
# It related to user cancel or complete order flow, which relies on the most recent orders being returned first.
|
|
44
44
|
if spree_current_user.present?
|
|
45
45
|
spree_current_user.orders
|
|
46
|
+
.exclude_vote_package
|
|
46
47
|
.not_archived
|
|
47
48
|
.incomplete
|
|
48
49
|
.order(id: :desc)
|
|
@@ -51,6 +52,7 @@ module Spree
|
|
|
51
52
|
return Spree::Order.none if order_tokens.empty?
|
|
52
53
|
|
|
53
54
|
Spree::Order.not_archived
|
|
55
|
+
.exclude_vote_package
|
|
54
56
|
.incomplete
|
|
55
57
|
.without_user
|
|
56
58
|
.where(token: order_tokens)
|
|
@@ -21,7 +21,7 @@ module SpreeCmCommissioner
|
|
|
21
21
|
@destination_place_id = @params.dig(:filter, :destination_place_id)
|
|
22
22
|
@query = @params.dig(:filter, :query) || @params[:query]
|
|
23
23
|
@limit = (@params[:limit] || @params.dig(:page, :limit) || 50).to_i
|
|
24
|
-
@order = @params[:order] || 'created_at
|
|
24
|
+
@order = @params[:order] || 'created_at ASC'
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def execute
|
|
@@ -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
|
|
@@ -15,14 +15,12 @@ module SpreeCmCommissioner
|
|
|
15
15
|
# "oidc-1") by default.
|
|
16
16
|
SUPPORTED_ALGORITHMS = %w[RS256 ES256].freeze
|
|
17
17
|
|
|
18
|
-
# :id_token
|
|
18
|
+
# :id_token, :client_id
|
|
19
19
|
def call
|
|
20
20
|
context.fail!(message: 'telegram_id_token_missing') if context.id_token.blank?
|
|
21
|
+
context.fail!(message: 'telegram_oauth_client_id_not_configured') if context.client_id.blank?
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
context.fail!(message: 'telegram_oauth_client_id_not_configured') if audience.blank?
|
|
24
|
-
|
|
25
|
-
claim = decode_id_token(audience)
|
|
23
|
+
claim = decode_id_token(context.client_id)
|
|
26
24
|
return if claim.nil?
|
|
27
25
|
|
|
28
26
|
context.claim = claim
|
|
@@ -83,10 +81,6 @@ module SpreeCmCommissioner
|
|
|
83
81
|
context.fail!(message: 'telegram_jwks_fetch_failed', error: e.message)
|
|
84
82
|
end
|
|
85
83
|
|
|
86
|
-
def expected_audience
|
|
87
|
-
ENV['TELEGRAM_OAUTH_CLIENT_ID'].presence
|
|
88
|
-
end
|
|
89
|
-
|
|
90
84
|
# sub is the Telegram user id (also the chat id once the user has /started
|
|
91
85
|
# the bot), which keeps OIDC logins consistent with the Telegram Web App
|
|
92
86
|
# flow that also keys UserIdentityProvider.telegram on the Telegram user id.
|
|
@@ -12,13 +12,17 @@ module SpreeCmCommissioner
|
|
|
12
12
|
# still requires `client_secret`, which is why it must be injected here
|
|
13
13
|
# server-side rather than sent by the app.
|
|
14
14
|
#
|
|
15
|
+
# `client_secret` is resolved by the caller (which has tenant context from
|
|
16
|
+
# the request) and passed in like any other param - this interactor only
|
|
17
|
+
# forwards it to Telegram, it never looks up a tenant/store itself.
|
|
18
|
+
#
|
|
15
19
|
# Docs: https://core.telegram.org/bots/telegram-login
|
|
16
20
|
class TelegramOauthTokenExchanger < BaseInteractor
|
|
17
21
|
TOKEN_ENDPOINT = 'https://oauth.telegram.org/token'.freeze
|
|
18
22
|
|
|
19
23
|
REQUIRED_PARAMS = %i[client_id code redirect_uri code_verifier].freeze
|
|
20
24
|
|
|
21
|
-
delegate :client_id, :code, :redirect_uri, :code_verifier, to: :context
|
|
25
|
+
delegate :client_id, :client_secret, :code, :redirect_uri, :code_verifier, to: :context
|
|
22
26
|
|
|
23
27
|
def call
|
|
24
28
|
missing = REQUIRED_PARAMS.find { |key| context[key].blank? }
|
|
@@ -33,10 +37,6 @@ module SpreeCmCommissioner
|
|
|
33
37
|
|
|
34
38
|
private
|
|
35
39
|
|
|
36
|
-
def client_secret
|
|
37
|
-
ENV['TELEGRAM_OAUTH_CLIENT_SECRET'].presence
|
|
38
|
-
end
|
|
39
|
-
|
|
40
40
|
def post_to_telegram
|
|
41
41
|
uri = URI(TOKEN_ENDPOINT)
|
|
42
42
|
request = Net::HTTP::Post.new(uri)
|
|
@@ -19,12 +19,22 @@ module SpreeCmCommissioner
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
21
|
def verified_provider
|
|
22
|
-
result = SpreeCmCommissioner::TelegramOauthIdTokenProvider.call(id_token: id_token)
|
|
22
|
+
result = SpreeCmCommissioner::TelegramOauthIdTokenProvider.call(id_token: id_token, client_id: telegram_client_id)
|
|
23
23
|
context.fail!(message: result.message) unless result.success?
|
|
24
24
|
|
|
25
25
|
result.provider
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# Each tenant runs its own Telegram bot, so the audience an id_token must
|
|
29
|
+
# carry is looked up on the tenant directly - no default-store or ENV
|
|
30
|
+
# fallback, so a misconfigured tenant fails loudly instead of silently
|
|
31
|
+
# authenticating against someone else's bot.
|
|
32
|
+
def telegram_client_id
|
|
33
|
+
return Spree::Store.default.preferred_telegram_oauth_client_id if tenant_id.blank?
|
|
34
|
+
|
|
35
|
+
SpreeCmCommissioner::Tenant.find_by(id: tenant_id)&.preferred_telegram_oauth_client_id
|
|
36
|
+
end
|
|
37
|
+
|
|
28
38
|
def find_or_register_user(provider)
|
|
29
39
|
identity_checker = SpreeCmCommissioner::UserIdentityChecker.call(
|
|
30
40
|
identity_type: provider[:identity_type],
|
|
@@ -3,6 +3,7 @@ module Spree
|
|
|
3
3
|
def cancel_email(order, resend: false)
|
|
4
4
|
@order = order.respond_to?(:id) ? order : Spree::Order.find(order)
|
|
5
5
|
return false if @order.email.blank?
|
|
6
|
+
return false if @order.products.all?(&:vote_package?) # disable cancel email for vote package orders
|
|
6
7
|
|
|
7
8
|
setup_tenant_and_store
|
|
8
9
|
if @tenant.present?
|
|
@@ -21,15 +21,25 @@ module SpreeCmCommissioner
|
|
|
21
21
|
# ⚠️ CRITICAL: Cached seat layout IDs kept in sync by SeatLayout callback.
|
|
22
22
|
# If callback is removed/modified without updating this, availability checks will fail.
|
|
23
23
|
#
|
|
24
|
-
#
|
|
24
|
+
# `preload_seat_layout_ids` is every seat layout ever attached to this event, regardless of
|
|
25
|
+
# status — kept for callers that need the full history/count. `preload_active_seat_layout_ids`
|
|
26
|
+
# is the subset with status `active`, so consumers checking "can this still be booked?"
|
|
27
|
+
# (e.g. hasSeatSelection on the app) should use that one instead — deactivating/archiving a
|
|
28
|
+
# layout removes it from this list even though it stays in `preload_seat_layout_ids`.
|
|
29
|
+
#
|
|
30
|
+
# Sync callback (see seat_layout.rb:L32-44):
|
|
25
31
|
# after_commit :sync_seat_layout_id_to_layoutable!, if: -> { layoutable.present? }
|
|
26
32
|
# def sync_seat_layout_id_to_layoutable!
|
|
27
|
-
# layoutable.update!(
|
|
33
|
+
# layoutable.update!(
|
|
34
|
+
# preload_seat_layout_ids: layoutable.seat_layouts.pluck(:id),
|
|
35
|
+
# preload_active_seat_layout_ids: layoutable.seat_layouts.active.pluck(:id)
|
|
36
|
+
# )
|
|
28
37
|
# end
|
|
29
38
|
#
|
|
30
39
|
# TODO: Not scalable for many seat layouts. Consider storing boolean flag
|
|
31
40
|
# (has_active_seat_layout) instead when seat layouts become frequent.
|
|
32
41
|
store_public_metadata :preload_seat_layout_ids, :array, default: []
|
|
42
|
+
store_public_metadata :preload_active_seat_layout_ids, :array, default: []
|
|
33
43
|
|
|
34
44
|
before_validation :validate_at_least_one_check_in_flow_presence
|
|
35
45
|
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',
|
|
@@ -281,6 +281,8 @@ module SpreeCmCommissioner
|
|
|
281
281
|
end
|
|
282
282
|
|
|
283
283
|
def notify_order_complete_app_notification_to_user
|
|
284
|
+
return if products.all?(&:vote_package?) # disable app push notification for vote package orders
|
|
285
|
+
|
|
284
286
|
SpreeCmCommissioner::OrderCompleteNotificationSender.call(order: self)
|
|
285
287
|
end
|
|
286
288
|
|
|
@@ -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
|
|
@@ -11,6 +11,8 @@ module SpreeCmCommissioner
|
|
|
11
11
|
preference :telegram_order_anomaly_alert_chat_id, :string
|
|
12
12
|
preference :assetlinks, :string, default: ''
|
|
13
13
|
preference :apple_app_site_association, :string, default: ''
|
|
14
|
+
preference :telegram_oauth_client_id, :string, default: ''
|
|
15
|
+
preference :telegram_oauth_client_secret, :string, default: ''
|
|
14
16
|
end
|
|
15
17
|
end
|
|
16
18
|
end
|
|
@@ -15,6 +15,8 @@ module SpreeCmCommissioner
|
|
|
15
15
|
preference :facebook, :string, default: ''
|
|
16
16
|
preference :twitter, :string, default: ''
|
|
17
17
|
preference :instagram, :string, default: ''
|
|
18
|
+
preference :telegram_oauth_client_id, :string, default: ''
|
|
19
|
+
preference :telegram_oauth_client_secret, :string, default: ''
|
|
18
20
|
end
|
|
19
21
|
end
|
|
20
22
|
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
|
|
|
@@ -33,11 +33,19 @@ module SpreeCmCommissioner
|
|
|
33
33
|
# Update layoutable's seat layout metadata after any change (create/update/destroy).
|
|
34
34
|
# This keeps the cache in sync with the actual seat_layouts association.
|
|
35
35
|
if layoutable.respond_to?(:seat_layouts)
|
|
36
|
-
# Event has many seat layouts:
|
|
37
|
-
|
|
36
|
+
# Event has many seat layouts: `..._ids` caches every layout ever attached (any status);
|
|
37
|
+
# `active_..._ids` caches only the `active` ones, for callers checking bookability.
|
|
38
|
+
layoutable.update!(
|
|
39
|
+
preload_seat_layout_ids: layoutable.seat_layouts.pluck(:id),
|
|
40
|
+
preload_active_seat_layout_ids: layoutable.seat_layouts.active.pluck(:id)
|
|
41
|
+
)
|
|
38
42
|
elsif layoutable.respond_to?(:seat_layout)
|
|
39
|
-
# Taxon/Vehicle has single seat layout: cache the ID (or nil if destroyed)
|
|
40
|
-
|
|
43
|
+
# Taxon/Vehicle has single seat layout: cache the ID (or nil if destroyed); the active
|
|
44
|
+
# variant is also nil while the layout is inactive/archived, not just when destroyed.
|
|
45
|
+
layoutable.update!(
|
|
46
|
+
preload_seat_layout_id: destroyed? ? nil : id,
|
|
47
|
+
preload_active_seat_layout_id: destroyed? || !active? ? nil : id
|
|
48
|
+
)
|
|
41
49
|
end
|
|
42
50
|
end
|
|
43
51
|
end
|
|
@@ -13,7 +13,9 @@ module SpreeCmCommissioner
|
|
|
13
13
|
has_many :vote_packages, -> { where(product_type: :vote_package) }, class_name: 'SpreeCmCommissioner::ShowEpisode', foreign_key: :event_id
|
|
14
14
|
has_many :voting_sessions, through: :episodes, class_name: 'SpreeCmCommissioner::VotingSession'
|
|
15
15
|
|
|
16
|
-
has_one :current_episode,
|
|
16
|
+
has_one :current_episode, lambda {
|
|
17
|
+
current_or_next_upcoming.where(status: :active)
|
|
18
|
+
}, class_name: 'SpreeCmCommissioner::ShowEpisode', foreign_key: :event_id
|
|
17
19
|
has_one :current_voting_session, through: :current_episode
|
|
18
20
|
|
|
19
21
|
before_validation { self.type = self.class.name }
|
|
@@ -17,7 +17,9 @@ module SpreeCmCommissioner
|
|
|
17
17
|
primary_key: :event_id
|
|
18
18
|
has_many :contestants, through: :season, source: :show_contestants
|
|
19
19
|
|
|
20
|
-
has_one :current_voting_session,
|
|
20
|
+
has_one :current_voting_session, lambda {
|
|
21
|
+
current_or_next_upcoming.where.not(status: :disabled)
|
|
22
|
+
}, class_name: 'SpreeCmCommissioner::VotingSession', foreign_key: :episode_id
|
|
21
23
|
|
|
22
24
|
validates :ends_at, presence: true, if: -> { product_type == 'show_episode' }
|
|
23
25
|
|
|
@@ -8,10 +8,15 @@ module SpreeCmCommissioner
|
|
|
8
8
|
|
|
9
9
|
enum trip_type: { direct: 0, multi_leg: 1 }
|
|
10
10
|
|
|
11
|
+
# public: shown in trip search results; unpublic: hidden from search (kept for the owning vendor).
|
|
12
|
+
# _prefix avoids generating a bare `public?` / `Trip.public` that would collide with Ruby's `public`.
|
|
13
|
+
enum visibility: { public: 0, unpublic: 1 }, _prefix: true, _default: :public
|
|
14
|
+
|
|
11
15
|
# This model has no seat_layout column (polymorphic association), so we store the preload_seat_layout_id ID in public_metadata.
|
|
12
16
|
# This lets us check if a seat layout exists without triggering a database query.
|
|
13
17
|
# The ID is automatically updated whenever the seat_layout is saved.
|
|
14
18
|
store_public_metadata :preload_seat_layout_id, :integer
|
|
19
|
+
store_public_metadata :preload_active_seat_layout_id, :integer
|
|
15
20
|
|
|
16
21
|
# Trip: array of offsets for each leg # => [0, 1, 2]
|
|
17
22
|
# Usage: trip.preload_offset_days_array[0] # => 0
|
|
@@ -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
|
|
@@ -7,6 +7,7 @@ module SpreeCmCommissioner
|
|
|
7
7
|
# This lets us check if a seat layout exists without triggering a database query.
|
|
8
8
|
# The ID is automatically updated whenever the seat_layout is saved.
|
|
9
9
|
store_public_metadata :preload_seat_layout_id, :integer
|
|
10
|
+
store_public_metadata :preload_active_seat_layout_id, :integer
|
|
10
11
|
|
|
11
12
|
attr_accessor :enable_seat_layout
|
|
12
13
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
<!-- insert_bottom "[data-hook='admin_store_form_fields']" -->
|
|
2
|
+
|
|
3
|
+
<% if @store.persisted? %>
|
|
4
|
+
<%
|
|
5
|
+
telegram_client_id = @store.preferred_telegram_oauth_client_id.presence
|
|
6
|
+
telegram_client_secret = @store.preferred_telegram_oauth_client_secret.presence
|
|
7
|
+
telegram_configured = telegram_client_id.present? && telegram_client_secret.present?
|
|
8
|
+
telegram_partial = telegram_client_id.present? ^ telegram_client_secret.present?
|
|
9
|
+
%>
|
|
10
|
+
<div class="card mb-4">
|
|
11
|
+
<div class="card-header d-flex flex-wrap align-items-center justify-content-between">
|
|
12
|
+
<h5 class="card-title mb-0 h6">Telegram Authentication</h5>
|
|
13
|
+
<% if telegram_configured %>
|
|
14
|
+
<span class="badge badge-success">Using this store's bot</span>
|
|
15
|
+
<% elsif telegram_partial %>
|
|
16
|
+
<span class="badge badge-warning">Incomplete</span>
|
|
17
|
+
<% else %>
|
|
18
|
+
<span class="badge badge-danger">Not configured</span>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
<div class="card-body">
|
|
22
|
+
<div id="telegram-auth-settings" data-hook class="form-group mb-0">
|
|
23
|
+
<div class="row">
|
|
24
|
+
<!-- Telegram OAuth Client ID -->
|
|
25
|
+
<div class="col-md-6">
|
|
26
|
+
<%= f.field_container :preferred_telegram_oauth_client_id do %>
|
|
27
|
+
<%= f.label :preferred_telegram_oauth_client_id, 'Client ID' %>
|
|
28
|
+
<%= f.text_field :preferred_telegram_oauth_client_id, class: 'form-control text-monospace',
|
|
29
|
+
placeholder: 'e.g. 7123456789', autocomplete: 'off' %>
|
|
30
|
+
<small class="form-text text-muted">Bot ID — digits before the colon in the BotFather token.</small>
|
|
31
|
+
<%= f.error_message_on :preferred_telegram_oauth_client_id %>
|
|
32
|
+
<% end %>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<!-- Telegram OAuth Client Secret -->
|
|
36
|
+
<div class="col-md-6">
|
|
37
|
+
<%= f.field_container :preferred_telegram_oauth_client_secret do %>
|
|
38
|
+
<%= f.label :preferred_telegram_oauth_client_secret, 'Client Secret' %>
|
|
39
|
+
<div class="input-group">
|
|
40
|
+
<%= f.password_field :preferred_telegram_oauth_client_secret,
|
|
41
|
+
class: 'form-control text-monospace', id: 'store_telegram_oauth_client_secret',
|
|
42
|
+
autocomplete: 'new-password', value: f.object.preferred_telegram_oauth_client_secret %>
|
|
43
|
+
<div class="input-group-append">
|
|
44
|
+
<button type="button" class="btn btn-outline-secondary"
|
|
45
|
+
data-telegram-secret-toggle="store_telegram_oauth_client_secret">
|
|
46
|
+
Show
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
<small class="form-text text-muted">From BotFather. Server-side only — never sent to the app.</small>
|
|
51
|
+
<%= f.error_message_on :preferred_telegram_oauth_client_secret %>
|
|
52
|
+
<% end %>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<script>
|
|
60
|
+
(function () {
|
|
61
|
+
document.querySelectorAll('[data-telegram-secret-toggle]').forEach(function (button) {
|
|
62
|
+
button.addEventListener('click', function () {
|
|
63
|
+
var field = document.getElementById(button.dataset.telegramSecretToggle);
|
|
64
|
+
if (!field) return;
|
|
65
|
+
|
|
66
|
+
var hidden = field.type === 'password';
|
|
67
|
+
field.type = hidden ? 'text' : 'password';
|
|
68
|
+
button.textContent = hidden ? 'Hide' : 'Show';
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
})();
|
|
72
|
+
</script>
|
|
73
|
+
<% end %>
|