spree_cm_commissioner 2.14.5 → 2.14.6
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 +1 -1
- data/Gemfile.lock +1 -1
- data/app/controllers/concerns/spree_cm_commissioner/content_cachable.rb +30 -7
- data/app/controllers/spree/admin/api_caches_controller.rb +23 -8
- data/app/controllers/spree/admin/homepage_section_relatable_controller.rb +24 -0
- data/app/controllers/spree/api/v2/organizer/invite_guests_controller.rb +2 -2
- data/app/controllers/spree/api/v2/resource_controller_decorator.rb +8 -0
- data/app/controllers/spree/api/v2/storefront/invite_guests_controller.rb +2 -2
- data/app/controllers/spree/api/v2/storefront/trip_search_controller.rb +18 -13
- data/app/controllers/spree/api/v2/storefront/user_contacts_controller.rb +2 -0
- data/app/controllers/spree/api/v2/tenant/trip_search_controller.rb +18 -13
- data/app/factory/spree_cm_commissioner/order_telegram_message_factory.rb +5 -1
- data/app/helpers/spree_cm_commissioner/admin/homepage_segment_helper.rb +28 -0
- data/app/interactors/spree_cm_commissioner/account_recover.rb +23 -2
- data/app/interactors/spree_cm_commissioner/firebase_id_token_provider.rb +6 -2
- data/app/interactors/spree_cm_commissioner/telegram_chats_auto_finder.rb +26 -2
- data/app/interactors/spree_cm_commissioner/telegram_chats_finder.rb +1 -1
- data/app/interactors/spree_cm_commissioner/user_telegram_oauth_authenticator.rb +2 -11
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +1 -13
- data/app/models/spree_cm_commissioner/maintenance_tasks/cache_invalidation.rb +95 -11
- data/app/models/spree_cm_commissioner/pin_code.rb +2 -1
- data/app/models/spree_cm_commissioner/pin_code_event_telegram.rb +31 -0
- data/app/models/spree_cm_commissioner/service_calendar.rb +2 -2
- data/app/models/spree_cm_commissioner/taxon_decorator.rb +7 -0
- data/app/models/spree_cm_commissioner/vendor_decorator.rb +6 -0
- data/app/queries/spree_cm_commissioner/trip_query.rb +20 -2
- data/app/services/spree_cm_commissioner/api_caches/fetch_details.rb +128 -0
- data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +31 -14
- data/app/services/spree_cm_commissioner/api_caches/invalidate_all.rb +16 -19
- data/app/services/spree_cm_commissioner/api_caches/request_clear.rb +71 -0
- data/app/services/spree_cm_commissioner/guests/claim_invite_guest_service.rb +5 -3
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_service_calendars.rb +3 -0
- data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_trips.rb +29 -4
- data/app/services/spree_cm_commissioner/inventory_items/bulk_generate_permanent_items.rb +15 -4
- data/app/services/spree_cm_commissioner/telegram/verify_oauth_id_token.rb +10 -0
- data/app/services/spree_cm_commissioner/telegram_alerts/send_telegram_order_alerts.rb +49 -0
- data/app/views/spree/admin/api_caches/_dialog.html.erb +47 -0
- data/app/views/spree/admin/api_caches/show.html.erb +88 -0
- data/app/views/spree/admin/homepage_section/edit.html.erb +16 -1
- data/app/views/spree/admin/shared/_api_cache_button.html.erb +5 -2
- data/config/locales/en.yml +39 -0
- data/config/locales/km.yml +39 -0
- data/config/routes.rb +3 -4
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +8 -3
- data/app/jobs/spree_cm_commissioner/invalidate_cache_request_job.rb +0 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 743a7213a36a799ebd7d68c8e2a770a9a253b9ca9e544c53dc89114344c35eec
|
|
4
|
+
data.tar.gz: c9697afd92b4175c73c1291224522b4b56fdf4056da7c8b4f441b33f0c828af4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0bc416a5439a60e3daaa23cb031fee13bd2a5ab82795e05aa52005713bd27adf298f0b09ae793d7109f2b3b55bd8fd0b937d50323b3f53031c64217ad1396bff
|
|
7
|
+
data.tar.gz: '073149c66ec7fc0b4b32d0c9b76ee35c934a96e489ae68ca6116adc4b60f831dc4271dd696ce996dbe6f80cd1db2658d0ffdeabfc25630e238d9c7cff5301bbd'
|
data/.env.example
CHANGED
|
@@ -49,7 +49,7 @@ DISTANCE_SIGNING_KEY=hei********************VY
|
|
|
49
49
|
|
|
50
50
|
# CDN cache durations (s-maxage, in seconds) for listed public APIs; client max-age is always 0, unlisted APIs are never CDN-cached
|
|
51
51
|
# See: app/controllers/concerns/spree_cm_commissioner/content_cachable.rb
|
|
52
|
-
CACHE_AUTO_INVALIDATION_DISABLED="" # Set to yes to
|
|
52
|
+
CACHE_AUTO_INVALIDATION_DISABLED="" # Set to yes to stop automatic CDN cache invalidation on save (kill switch); a clear-cache click still sends one
|
|
53
53
|
CACHE_SERIALIZER_DISABLED="" # Set to yes to turn off Spree's serializer record cache everywhere
|
|
54
54
|
CACHE_STATIC_MAX_AGE=86400 # Static content (CMS pages) - 1 day
|
|
55
55
|
CACHE_REALTIME_MAX_AGE=300 # Everything else listed (homepage, menus, products, events, vendors, trips, seat layouts, countries, provinces, guest card classes) - 5 minutes
|
data/Gemfile.lock
CHANGED
|
@@ -74,14 +74,20 @@ module SpreeCmCommissioner
|
|
|
74
74
|
'Spree::Api::V2::Tenant::VotingContestantsController' => { actions: %w[index].freeze, max_age: :fresh }
|
|
75
75
|
}.freeze
|
|
76
76
|
|
|
77
|
-
#
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
# Seconds for a TTL tier, or nil for none. Public so the clear-cache dialog can say how long an
|
|
78
|
+
# edit takes to show without a clear.
|
|
79
|
+
def self.cdn_max_age(tier)
|
|
80
|
+
case tier
|
|
80
81
|
when :static then ENV.fetch('CACHE_STATIC_MAX_AGE', 1.day.to_i).to_i
|
|
81
82
|
when :fresh then ENV.fetch('CACHE_REALTIME_MAX_AGE', 5.minutes.to_i).to_i
|
|
82
83
|
end
|
|
83
84
|
end
|
|
84
85
|
|
|
86
|
+
# CDN (s-maxage) TTL, or nil for an unlisted controller. Override in a controller for a different one.
|
|
87
|
+
def shared_max_age
|
|
88
|
+
ContentCachable.cdn_max_age(cdn_cache_config&.dig(:max_age))
|
|
89
|
+
end
|
|
90
|
+
|
|
85
91
|
def cdn_cacheable?
|
|
86
92
|
request.get? && cdn_cache_config.present? && cdn_cache_config[:actions].include?(action_name)
|
|
87
93
|
end
|
|
@@ -102,12 +108,10 @@ module SpreeCmCommissioner
|
|
|
102
108
|
# list so a new secret can be added to CloudFront before the old one is dropped. With
|
|
103
109
|
# CDN_ORIGIN_REPORT_ONLY=yes it only logs, to find clients still calling the origin before enforcing.
|
|
104
110
|
def verify_cdn_origin!
|
|
105
|
-
|
|
106
|
-
return if
|
|
111
|
+
return if cdn_origin_secrets.empty?
|
|
112
|
+
return if cdn_origin_verified?
|
|
107
113
|
|
|
108
114
|
provided = request.headers[CDN_ORIGIN_HEADER].to_s
|
|
109
|
-
return if secrets.any? { |secret| ActiveSupport::SecurityUtils.secure_compare(provided, secret) }
|
|
110
|
-
|
|
111
115
|
if ENV['CDN_ORIGIN_REPORT_ONLY'] == 'yes'
|
|
112
116
|
CmAppLogger.log(
|
|
113
117
|
label: 'cdn_origin_unverified',
|
|
@@ -125,8 +129,27 @@ module SpreeCmCommissioner
|
|
|
125
129
|
head :forbidden
|
|
126
130
|
end
|
|
127
131
|
|
|
132
|
+
# True when CloudFront will cache this response, so a controller-level Rails.cache in front of it
|
|
133
|
+
# only stacks its TTL on CloudFront's and hands a stale body to the refetch that follows a
|
|
134
|
+
# CloudFront invalidation. Requires a matching secret, not just the header: a header any caller
|
|
135
|
+
# can set must not let them skip both caches and hit the query directly.
|
|
136
|
+
def served_to_cdn?
|
|
137
|
+
return @served_to_cdn if defined?(@served_to_cdn)
|
|
138
|
+
|
|
139
|
+
@served_to_cdn = cdn_cacheable? && cdn_origin_verified?
|
|
140
|
+
end
|
|
141
|
+
|
|
128
142
|
private
|
|
129
143
|
|
|
144
|
+
def cdn_origin_verified?
|
|
145
|
+
provided = request.headers[CDN_ORIGIN_HEADER].to_s
|
|
146
|
+
cdn_origin_secrets.any? { |secret| ActiveSupport::SecurityUtils.secure_compare(provided, secret) }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def cdn_origin_secrets
|
|
150
|
+
ENV.fetch('CDN_ORIGIN_SECRETS', '').split(',').map(&:strip).compact_blank
|
|
151
|
+
end
|
|
152
|
+
|
|
130
153
|
def cdn_cache_config
|
|
131
154
|
CDN_CACHEABLE_ACTIONS[self.class.name.to_s]
|
|
132
155
|
end
|
|
@@ -2,16 +2,16 @@ module Spree
|
|
|
2
2
|
module Admin
|
|
3
3
|
# Behind every admin clear-cache button (spree/admin/shared/_api_cache_button). Admin-only through
|
|
4
4
|
# BaseController#authorize_admin, which checks :api_caches since there is no model class.
|
|
5
|
+
#
|
|
6
|
+
# Both actions take model and id, or no id to clear every record of model on one storefront:
|
|
7
|
+
# tenant_id for that tenant's API, none for the BookMe+ storefront.
|
|
5
8
|
class ApiCachesController < Spree::Admin::BaseController
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
before_action :load_tenant
|
|
10
|
+
|
|
8
11
|
def clear
|
|
9
|
-
result =
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
tenant = SpreeCmCommissioner::Tenant.find(params[:tenant_id]) if params[:tenant_id].present?
|
|
13
|
-
SpreeCmCommissioner::ApiCaches::InvalidateAll.call(model: params[:model], tenant: tenant)
|
|
14
|
-
end
|
|
12
|
+
result = SpreeCmCommissioner::ApiCaches::RequestClear.call(
|
|
13
|
+
model: params[:model], id: params[:id], tenant: @tenant, user: try_spree_current_user
|
|
14
|
+
)
|
|
15
15
|
|
|
16
16
|
if result.success?
|
|
17
17
|
flash[:success] = Spree.t(:clear_cache_requested)
|
|
@@ -21,6 +21,21 @@ module Spree
|
|
|
21
21
|
|
|
22
22
|
redirect_back fallback_location: admin_root_path
|
|
23
23
|
end
|
|
24
|
+
|
|
25
|
+
# The dialog's contents, loaded into spree/admin/api_caches/_dialog's frame by the button.
|
|
26
|
+
def show
|
|
27
|
+
result = SpreeCmCommissioner::ApiCaches::FetchDetails.call(model: params[:model], id: params[:id], tenant: @tenant)
|
|
28
|
+
raise ActiveRecord::RecordNotFound if result.failure?
|
|
29
|
+
|
|
30
|
+
@details = result.value
|
|
31
|
+
render layout: false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def load_tenant
|
|
37
|
+
@tenant = SpreeCmCommissioner::Tenant.find(params[:tenant_id]) if params[:tenant_id].present?
|
|
38
|
+
end
|
|
24
39
|
end
|
|
25
40
|
end
|
|
26
41
|
end
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module Admin
|
|
3
3
|
class HomepageSectionRelatableController < Spree::Admin::ResourceController
|
|
4
|
+
skip_before_action :load_resource, only: :sort_by_start_date
|
|
5
|
+
|
|
4
6
|
def model_class
|
|
5
7
|
SpreeCmCommissioner::HomepageSectionRelatable
|
|
6
8
|
end
|
|
@@ -27,6 +29,28 @@ module Spree
|
|
|
27
29
|
def permitted_resource_params
|
|
28
30
|
params.require(object_name).permit(:homepage_section_id, :relatable_id, :relatable_type, :available_on, :discontinue_on)
|
|
29
31
|
end
|
|
32
|
+
|
|
33
|
+
# One-shot bulk reorder: ranks taxon relatables with an event `from_date` ascending, keeps every other
|
|
34
|
+
# relatable (dateless taxons, products) at the bottom in its current relative order. Drag-and-drop remains
|
|
35
|
+
# available afterward and simply overwrites these positions again, same as it would after a manual reorder.
|
|
36
|
+
def sort_by_start_date
|
|
37
|
+
homepage_section = SpreeCmCommissioner::HomepageSection.find(params[:homepage_section_id])
|
|
38
|
+
relatables = homepage_section.homepage_section_relatables.includes(:relatable).to_a
|
|
39
|
+
|
|
40
|
+
dated, undated = relatables.partition { |relatable| relatable.relatable.is_a?(Spree::Taxon) && relatable.relatable.from_date.present? }
|
|
41
|
+
dated.sort_by! { |relatable| relatable.relatable.from_date }
|
|
42
|
+
undated.sort_by!(&:position)
|
|
43
|
+
|
|
44
|
+
SpreeCmCommissioner::HomepageSectionRelatable.transaction do
|
|
45
|
+
(dated + undated).each.with_index(1) do |relatable, position|
|
|
46
|
+
relatable.update_columns(position: position, updated_at: Time.current) # rubocop:disable Rails/SkipsModelValidations
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
homepage_section.update_columns(updated_at: Time.current) # rubocop:disable Rails/SkipsModelValidations
|
|
50
|
+
|
|
51
|
+
redirect_to edit_admin_homepage_feed_homepage_section_url(homepage_section),
|
|
52
|
+
notice: Spree.t(:successfully_updated, resource: 'Homepage Section Relatable')
|
|
53
|
+
end
|
|
30
54
|
end
|
|
31
55
|
end
|
|
32
56
|
end
|
|
@@ -20,7 +20,7 @@ module Spree
|
|
|
20
20
|
|
|
21
21
|
if guest.save
|
|
22
22
|
@invite_guest.update(claimed_status: :claimed) if @line_item.guests.count == @invite_guest.quantity
|
|
23
|
-
send_guest_claimed_invitation_telegram_alert_to_vendor(guest)
|
|
23
|
+
send_guest_claimed_invitation_telegram_alert_to_vendor(guest)
|
|
24
24
|
|
|
25
25
|
render json: SpreeCmCommissioner::V2::Storefront::GuestSerializer.new(guest.reload).serializable_hash
|
|
26
26
|
else
|
|
@@ -32,7 +32,7 @@ module Spree
|
|
|
32
32
|
|
|
33
33
|
def send_guest_claimed_invitation_telegram_alert_to_vendor(guest)
|
|
34
34
|
title = '📣 --- [NEW GUEST CLAIMED INVITATION] ---' # Style/StringLiterals
|
|
35
|
-
chat_id = guest.event.vendor.preferred_telegram_chat_id
|
|
35
|
+
chat_id = guest.event.telegram_alert_chat_id || guest.event.vendor.preferred_telegram_chat_id
|
|
36
36
|
return if chat_id.blank?
|
|
37
37
|
|
|
38
38
|
factory = SpreeCmCommissioner::InviteGuestClaimedTelegramMessageFactory.new(
|
|
@@ -10,6 +10,14 @@ module Spree
|
|
|
10
10
|
base.attr_reader :schema
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
# override: Spree wraps this in a Rails.cache.fetch keyed on ids + max updated_at (1h TTL),
|
|
14
|
+
# which goes stale when only an association changes. See ContentCachable#served_to_cdn?.
|
|
15
|
+
def index
|
|
16
|
+
return render_serialized_payload { serialize_collection(paginated_collection) } if served_to_cdn?
|
|
17
|
+
|
|
18
|
+
super
|
|
19
|
+
end
|
|
20
|
+
|
|
13
21
|
def app_version
|
|
14
22
|
request.headers['X-Cm-App-Version']
|
|
15
23
|
end
|
|
@@ -32,7 +32,7 @@ module Spree
|
|
|
32
32
|
|
|
33
33
|
def send_guest_claimed_invitation_telegram_alert_to_vendor(guest)
|
|
34
34
|
title = '📣 --- [NEW GUEST CLAIMED INVITATION] ---' # Style/StringLiterals
|
|
35
|
-
chat_id = guest.event&.vendor&.preferred_telegram_chat_id
|
|
35
|
+
chat_id = guest.event&.telegram_alert_chat_id || guest.event&.vendor&.preferred_telegram_chat_id
|
|
36
36
|
return if chat_id.blank?
|
|
37
37
|
|
|
38
38
|
factory = SpreeCmCommissioner::InviteGuestClaimedTelegramMessageFactory.new(
|
|
@@ -97,7 +97,7 @@ module Spree
|
|
|
97
97
|
def finalize_guest_claim(guest)
|
|
98
98
|
if guest.save
|
|
99
99
|
@invite_guest.update(claimed_status: :claimed) if @line_item.guests.count == @invite_guest.quantity
|
|
100
|
-
send_guest_claimed_invitation_telegram_alert_to_vendor(guest)
|
|
100
|
+
send_guest_claimed_invitation_telegram_alert_to_vendor(guest)
|
|
101
101
|
|
|
102
102
|
order = @invite_guest.order
|
|
103
103
|
render_serialized_payload { serialize_resource(order) }
|
|
@@ -6,25 +6,30 @@ module Spree
|
|
|
6
6
|
CACHE_EXPIRES_IN = 5.minutes
|
|
7
7
|
|
|
8
8
|
def index
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
trips = SpreeCmCommissioner::TripQuery.new(
|
|
12
|
-
origin_id: params[:origin_id],
|
|
13
|
-
destination_id: params[:destination_id],
|
|
14
|
-
date: params[:date],
|
|
15
|
-
route_type: params[:route_type],
|
|
16
|
-
vendor_id: params[:vendor_id],
|
|
17
|
-
number_of_guests: params[:number_of_guests],
|
|
18
|
-
options: params
|
|
19
|
-
).call
|
|
9
|
+
# CloudFront already caches this action; see ContentCachable#served_to_cdn?.
|
|
10
|
+
return render_serialized_payload { search_and_serialize } if served_to_cdn?
|
|
20
11
|
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
render_serialized_payload do
|
|
13
|
+
Rails.cache.fetch(collection_cache_key, collection_cache_opts) { search_and_serialize }
|
|
23
14
|
end
|
|
24
15
|
end
|
|
25
16
|
|
|
26
17
|
private
|
|
27
18
|
|
|
19
|
+
def search_and_serialize
|
|
20
|
+
trips = SpreeCmCommissioner::TripQuery.new(
|
|
21
|
+
origin_id: params[:origin_id],
|
|
22
|
+
destination_id: params[:destination_id],
|
|
23
|
+
date: params[:date],
|
|
24
|
+
route_type: params[:route_type],
|
|
25
|
+
vendor_id: params[:vendor_id],
|
|
26
|
+
number_of_guests: params[:number_of_guests],
|
|
27
|
+
options: params
|
|
28
|
+
).call
|
|
29
|
+
|
|
30
|
+
serialize_collection(trips)
|
|
31
|
+
end
|
|
32
|
+
|
|
28
33
|
# override
|
|
29
34
|
def collection_cache_key
|
|
30
35
|
cache_key_parts = [
|
|
@@ -6,25 +6,30 @@ module Spree
|
|
|
6
6
|
CACHE_EXPIRES_IN = 5.minutes
|
|
7
7
|
|
|
8
8
|
def index
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
trips = SpreeCmCommissioner::TripQuery.new(
|
|
12
|
-
origin_id: params[:origin_id],
|
|
13
|
-
destination_id: params[:destination_id],
|
|
14
|
-
date: params[:date],
|
|
15
|
-
route_type: params[:route_type],
|
|
16
|
-
number_of_guests: params[:number_of_guests],
|
|
17
|
-
tenant_id: @tenant.id,
|
|
18
|
-
options: params
|
|
19
|
-
).call
|
|
9
|
+
# CloudFront already caches this action; see ContentCachable#served_to_cdn?.
|
|
10
|
+
return render_serialized_payload { search_and_serialize } if served_to_cdn?
|
|
20
11
|
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
render_serialized_payload do
|
|
13
|
+
Rails.cache.fetch(collection_cache_key, collection_cache_opts) { search_and_serialize }
|
|
23
14
|
end
|
|
24
15
|
end
|
|
25
16
|
|
|
26
17
|
private
|
|
27
18
|
|
|
19
|
+
def search_and_serialize
|
|
20
|
+
trips = SpreeCmCommissioner::TripQuery.new(
|
|
21
|
+
origin_id: params[:origin_id],
|
|
22
|
+
destination_id: params[:destination_id],
|
|
23
|
+
date: params[:date],
|
|
24
|
+
route_type: params[:route_type],
|
|
25
|
+
number_of_guests: params[:number_of_guests],
|
|
26
|
+
tenant_id: @tenant.id,
|
|
27
|
+
options: params
|
|
28
|
+
).call
|
|
29
|
+
|
|
30
|
+
serialize_collection(trips)
|
|
31
|
+
end
|
|
32
|
+
|
|
28
33
|
# override
|
|
29
34
|
def collection_cache_key
|
|
30
35
|
cache_key_parts = [
|
|
@@ -26,15 +26,19 @@ module SpreeCmCommissioner
|
|
|
26
26
|
class OrderTelegramMessageFactory < TelegramMessageFactory # rubocop:disable Metrics/ClassLength
|
|
27
27
|
attr_reader :order, :vendor, :show_details_link
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
# `line_items` narrows the message to an explicit subset and wins over `vendor`: an event with its own
|
|
30
|
+
# group takes its items out of the vendor's message, so neither message is simply "all vendor items".
|
|
31
|
+
def initialize(title:, order:, subtitle: nil, show_details_link: nil, vendor: nil, line_items: nil) # rubocop:disable Metrics/ParameterLists
|
|
30
32
|
@order = order
|
|
31
33
|
@vendor = vendor
|
|
34
|
+
@line_items = line_items
|
|
32
35
|
@show_details_link = show_details_link || false
|
|
33
36
|
|
|
34
37
|
super(title: title, subtitle: subtitle)
|
|
35
38
|
end
|
|
36
39
|
|
|
37
40
|
def selected_line_items
|
|
41
|
+
return @line_items if @line_items
|
|
38
42
|
return order.line_items.for_vendor(vendor) if vendor.present?
|
|
39
43
|
|
|
40
44
|
order.line_items
|
|
@@ -43,8 +43,36 @@ module SpreeCmCommissioner
|
|
|
43
43
|
calculate_bitwise_value(params, SpreeCmCommissioner::HomepageSectionBitwise::BIT_MINI_APP)
|
|
44
44
|
end
|
|
45
45
|
|
|
46
|
+
# Only event-kind taxons carry `from_date`/`to_date` (see TaxonDecorator); plain category taxons and
|
|
47
|
+
# products return nil here so the caller can fall back to a placeholder.
|
|
48
|
+
def homepage_section_relatable_event_dates(section_relatable)
|
|
49
|
+
taxon = section_relatable.relatable
|
|
50
|
+
return unless taxon.is_a?(Spree::Taxon) && taxon.from_date.present?
|
|
51
|
+
|
|
52
|
+
range = taxon.to_date.present? ? "#{pretty_date(taxon.from_date)} – #{pretty_date(taxon.to_date)}" : pretty_date(taxon.from_date)
|
|
53
|
+
safe_join([content_tag(:div, range), content_tag(:small, event_date_status_label(taxon), class: 'text-muted')])
|
|
54
|
+
end
|
|
55
|
+
|
|
46
56
|
private
|
|
47
57
|
|
|
58
|
+
# Date-only (no time-of-day/timezone), unlike `pretty_time` — a list column scanned for date ranges
|
|
59
|
+
# doesn't need per-row clock precision, and the full timestamp roughly doubled this column's width.
|
|
60
|
+
def pretty_date(time)
|
|
61
|
+
time.to_date.strftime('%d %B %Y')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def event_date_status_label(taxon)
|
|
65
|
+
now = Time.current
|
|
66
|
+
|
|
67
|
+
if taxon.from_date > now
|
|
68
|
+
"Starts in #{distance_of_time_in_words(now, taxon.from_date)}"
|
|
69
|
+
elsif taxon.to_date.present? && taxon.to_date > now
|
|
70
|
+
"Ongoing · ends in #{distance_of_time_in_words(now, taxon.to_date)}"
|
|
71
|
+
else
|
|
72
|
+
"Ended #{distance_of_time_in_words(now, taxon.to_date || taxon.from_date)} ago"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
48
76
|
# Sums the checked boxes into one integer. Keyed by name, not position, so a checkbox the
|
|
49
77
|
# form omits cannot shift the rest onto the wrong bits. Returns 0, never nil: `value & bit`
|
|
50
78
|
# raises on NULL.
|
|
@@ -18,8 +18,7 @@ module SpreeCmCommissioner
|
|
|
18
18
|
context.user = Spree.user_class.find_user_by_login(login, tenant_id)
|
|
19
19
|
# get social user
|
|
20
20
|
elsif id_token.present?
|
|
21
|
-
|
|
22
|
-
context.user = checker_context.user
|
|
21
|
+
context.user = id_token_user
|
|
23
22
|
end
|
|
24
23
|
|
|
25
24
|
if context.user.present?
|
|
@@ -28,5 +27,27 @@ module SpreeCmCommissioner
|
|
|
28
27
|
context.fail!(message: 'User is not valid')
|
|
29
28
|
end
|
|
30
29
|
end
|
|
30
|
+
|
|
31
|
+
def id_token_user
|
|
32
|
+
if telegram_id_token?
|
|
33
|
+
result = SpreeCmCommissioner::Telegram::CheckIdToken.call(
|
|
34
|
+
id_token: id_token,
|
|
35
|
+
client_id: SpreeCmCommissioner::Telegram::VerifyOauthIdToken.client_id_for(tenant_id),
|
|
36
|
+
tenant_id: tenant_id
|
|
37
|
+
)
|
|
38
|
+
result.value[:user] if result.success?
|
|
39
|
+
else
|
|
40
|
+
SpreeCmCommissioner::UserIdTokenChecker.call(id_token: id_token).user
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The app sends Telegram OIDC tokens here as `id_token` too. The unverified `iss` only picks the
|
|
45
|
+
# checker; CheckIdToken still verifies the signature, issuer, audience and expiry before trusting it.
|
|
46
|
+
def telegram_id_token?
|
|
47
|
+
payload = JWT.decode(id_token, nil, false).first
|
|
48
|
+
payload.is_a?(Hash) && payload['iss'] == SpreeCmCommissioner::Telegram::VerifyOauthIdToken::ISSUER
|
|
49
|
+
rescue JWT::DecodeError
|
|
50
|
+
false
|
|
51
|
+
end
|
|
31
52
|
end
|
|
32
53
|
end
|
|
@@ -84,9 +84,13 @@ module SpreeCmCommissioner
|
|
|
84
84
|
|
|
85
85
|
return nil if claim.nil?
|
|
86
86
|
|
|
87
|
-
|
|
87
|
+
# A non-Firebase token (e.g. a Telegram OIDC id_token, or a crafted non-object payload)
|
|
88
|
+
# has no `firebase` section.
|
|
89
|
+
provider_name = claim.dig('firebase', 'sign_in_provider') if claim.is_a?(Hash)
|
|
90
|
+
context.fail!(message: I18n.t('firebase_id_token.failure')) if provider_name.blank?
|
|
91
|
+
|
|
88
92
|
user_id = claim['user_id']
|
|
89
|
-
sub = claim
|
|
93
|
+
sub = claim.dig('firebase', 'identities', provider_name)&.first
|
|
90
94
|
email = claim['email'] || get_user_email(user_id)
|
|
91
95
|
name = claim['name']
|
|
92
96
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
class TelegramChatsAutoFinder < BaseInteractor
|
|
3
|
-
|
|
3
|
+
# Pass `event:` to connect that event's alert group instead of the vendor's (contact is then
|
|
4
|
+
# the event id). Event mode writes only the event's metadata: no vendor preference and no
|
|
5
|
+
# vendor-owned TelegramChat row.
|
|
6
|
+
delegate :contact, :pin_code, :event, to: :context
|
|
4
7
|
|
|
5
8
|
def call
|
|
6
9
|
context.chats = []
|
|
@@ -59,6 +62,8 @@ module SpreeCmCommissioner
|
|
|
59
62
|
pin_code_record = find_pin_code_record
|
|
60
63
|
return unless pin_code_record
|
|
61
64
|
|
|
65
|
+
return connect_event(message, chat_id, pin_code_record) if event
|
|
66
|
+
|
|
62
67
|
vendor = find_vendor
|
|
63
68
|
return unless vendor
|
|
64
69
|
|
|
@@ -73,7 +78,8 @@ module SpreeCmCommissioner
|
|
|
73
78
|
end
|
|
74
79
|
|
|
75
80
|
def find_pin_code_record
|
|
76
|
-
|
|
81
|
+
pin_code_class = event ? SpreeCmCommissioner::PinCodeEventTelegram : SpreeCmCommissioner::PinCodeTelegram
|
|
82
|
+
pin_code_record = pin_code_class.find_by(code: pin_code, contact: contact)
|
|
77
83
|
unless pin_code_record
|
|
78
84
|
context.fail!(message: "Pin code #{pin_code} not found for contact #{contact}")
|
|
79
85
|
return nil
|
|
@@ -114,6 +120,24 @@ module SpreeCmCommissioner
|
|
|
114
120
|
context.verified_chat_ids << chat.chat_id
|
|
115
121
|
end
|
|
116
122
|
|
|
123
|
+
def connect_event(message, chat_id, pin_code_record)
|
|
124
|
+
title = message.dig('chat', 'title') || 'Unnamed Chat'
|
|
125
|
+
|
|
126
|
+
unless event.update(telegram_alert_chat_id: chat_id.to_s, telegram_alert_chat_name: title,
|
|
127
|
+
telegram_alert_chat_type: message.dig('chat', 'type')
|
|
128
|
+
)
|
|
129
|
+
context.fail!(message: "Failed to connect event: #{event.errors.full_messages.join(', ')}")
|
|
130
|
+
return
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
context.chats << { id: chat_id, title: title, type: message.dig('chat', 'type') }
|
|
134
|
+
expire_pin_code(pin_code_record)
|
|
135
|
+
send_message(chat_id, "✅ PIN code verified successfully! This chat is now connected to <b>#{CGI.escapeHTML(event.name.to_s)}</b>.\n\n" \
|
|
136
|
+
'Completed orders for this event will be sent to this chat.'
|
|
137
|
+
)
|
|
138
|
+
context.verified_chat_ids << chat_id
|
|
139
|
+
end
|
|
140
|
+
|
|
117
141
|
def update_vendor_preferences(vendor, chat)
|
|
118
142
|
vendor.update(
|
|
119
143
|
preferred_telegram_chat_id: chat.chat_id,
|
|
@@ -11,7 +11,7 @@ module SpreeCmCommissioner
|
|
|
11
11
|
updates['result'].filter_map do |update|
|
|
12
12
|
chat = update.dig('my_chat_member', 'chat')
|
|
13
13
|
|
|
14
|
-
if chat.present? && chat['title'] == telegram_chat_name.strip && chat['type'] == telegram_chat_type
|
|
14
|
+
if chat.present? && chat['title'] == telegram_chat_name.to_s.strip && chat['type'] == telegram_chat_type
|
|
15
15
|
return {
|
|
16
16
|
id: chat['id'],
|
|
17
17
|
title: chat['title'],
|
|
@@ -22,22 +22,13 @@ module SpreeCmCommissioner
|
|
|
22
22
|
private
|
|
23
23
|
|
|
24
24
|
def verified_provider
|
|
25
|
-
|
|
25
|
+
client_id = SpreeCmCommissioner::Telegram::VerifyOauthIdToken.client_id_for(tenant_id)
|
|
26
|
+
result = SpreeCmCommissioner::Telegram::VerifyOauthIdToken.call(id_token: id_token, client_id: client_id)
|
|
26
27
|
context.fail!(message: result.error.to_s) unless result.success?
|
|
27
28
|
|
|
28
29
|
result.value[:provider]
|
|
29
30
|
end
|
|
30
31
|
|
|
31
|
-
# Each tenant runs its own Telegram bot, so the audience an id_token must
|
|
32
|
-
# carry is looked up on the tenant directly - no default-store or ENV
|
|
33
|
-
# fallback, so a misconfigured tenant fails loudly instead of silently
|
|
34
|
-
# authenticating against someone else's bot.
|
|
35
|
-
def telegram_client_id
|
|
36
|
-
return Spree::Store.default.preferred_telegram_oauth_client_id if tenant_id.blank?
|
|
37
|
-
|
|
38
|
-
SpreeCmCommissioner::Tenant.find_by(id: tenant_id)&.preferred_telegram_oauth_client_id
|
|
39
|
-
end
|
|
40
|
-
|
|
41
32
|
def find_or_register_user(provider)
|
|
42
33
|
identity_checker = SpreeCmCommissioner::UserIdentityChecker.call(
|
|
43
34
|
identity_type: provider[:identity_type],
|
|
@@ -345,19 +345,7 @@ module SpreeCmCommissioner
|
|
|
345
345
|
end
|
|
346
346
|
|
|
347
347
|
def send_order_complete_telegram_alert_to_vendors
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
vendor_list.each do |vendor|
|
|
351
|
-
# `next`, not `return`: the mask is per-vendor, so one opted-out vendor on a
|
|
352
|
-
# multi-vendor order must not suppress the others.
|
|
353
|
-
next unless vendor.alerts_telegram_for_channel?(channel_key)
|
|
354
|
-
|
|
355
|
-
chat_id = vendor.preferred_telegram_chat_id
|
|
356
|
-
next if chat_id.blank?
|
|
357
|
-
|
|
358
|
-
factory = OrderTelegramMessageFactory.new(title: vendor_alert_title(vendor), order: self, vendor: vendor)
|
|
359
|
-
TelegramNotificationSenderJob.perform_later(chat_id: chat_id, message: factory.message, parse_mode: factory.parse_mode)
|
|
360
|
-
end
|
|
348
|
+
SpreeCmCommissioner::TelegramAlerts::SendTelegramOrderAlerts.call(order: self)
|
|
361
349
|
end
|
|
362
350
|
|
|
363
351
|
# White-label vendors announce their own brand; platform vendors stay BookMe+.
|