spree_cm_commissioner 2.5.16.pre.pre8 → 2.5.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +29 -0
  4. data/app/controllers/concerns/spree_cm_commissioner/turnstile_protectable.rb +42 -0
  5. data/app/controllers/spree/api/v2/organizer/invite_guests_controller.rb +91 -0
  6. data/app/controllers/spree/api/v2/storefront/saved_guests_controller.rb +77 -0
  7. data/app/helpers/spree_cm_commissioner/transit/trip_helper.rb +0 -28
  8. data/app/interactors/spree_cm_commissioner/create_vendor.rb +2 -0
  9. data/app/interactors/spree_cm_commissioner/turnstile_token_validator.rb +55 -0
  10. data/app/jobs/spree_cm_commissioner/cleanup_expired_access_tokens_job.rb +9 -0
  11. data/app/models/spree_cm_commissioner/agency.rb +12 -0
  12. data/app/models/spree_cm_commissioner/role_decorator.rb +3 -0
  13. data/app/models/spree_cm_commissioner/trip.rb +0 -2
  14. data/app/models/spree_cm_commissioner/user_decorator.rb +1 -0
  15. data/app/queries/spree_cm_commissioner/trip_query.rb +128 -28
  16. data/app/serializers/spree/v2/organizer/invite_guest_serializer.rb +13 -0
  17. data/app/serializers/spree/v2/tenant/line_item_serializer.rb +1 -1
  18. data/app/services/spree_cm_commissioner/cleanup_expired_access_tokens.rb +52 -0
  19. data/app/services/spree_cm_commissioner/transit_order/create.rb +6 -48
  20. data/app/services/spree_cm_commissioner/trips/create_single_leg.rb +1 -2
  21. data/app/services/spree_cm_commissioner/trips/search.rb +8 -12
  22. data/config/locales/en.yml +5 -0
  23. data/config/locales/km.yml +5 -0
  24. data/config/routes.rb +2 -0
  25. data/db/migrate/20260318081516_create_cm_agencies.rb +18 -0
  26. data/db/migrate/20260319090000_add_role_type_to_spree_roles.rb +5 -0
  27. data/db/migrate/20260320103313_add_index_to_spree_oauth_access_tokens_created_at.rb +7 -0
  28. data/lib/spree_cm_commissioner/transit/service_calendar_form.rb +0 -19
  29. data/lib/spree_cm_commissioner/trip_query_result.rb +6 -30
  30. data/lib/spree_cm_commissioner/trip_result.rb +0 -55
  31. data/lib/spree_cm_commissioner/version.rb +1 -1
  32. metadata +15 -9
  33. data/app/queries/spree_cm_commissioner/multi_leg_trips_query.rb +0 -292
  34. data/app/queries/spree_cm_commissioner/single_leg_trips_query.rb +0 -91
  35. data/app/services/spree_cm_commissioner/trips/create_multi_leg.rb +0 -542
  36. data/app/services/spree_cm_commissioner/trips/preload_inventory.rb +0 -80
  37. data/db/migrate/20260226105108_add_trip_type_to_cm_trip.rb +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 659785b7538c0a039babbe93d0c14a08171ecc75c4131cb0de992da84d3bfac0
4
- data.tar.gz: 548dff81e18f044aaedb94cc3a886cfd2c2600338a6760fc2e2c1ef04d75508c
3
+ metadata.gz: b8c433f50fb2876095dad4e8adbf87547a04c0bdef08f1db15f545a5f3539aa2
4
+ data.tar.gz: 3af1f877767be983922d340cd0249c3bfbf897e6e544ffaae3ea2bb37e00bd82
5
5
  SHA512:
6
- metadata.gz: 43a7f398986a3b4ed009505986cd705d7556d9b2413f8ff003ab7f9d0bd099e52b69bd86717fd4b927e87fd1bfa4e5e1b2b6df39233b83cb1f2685dc27613b3e
7
- data.tar.gz: e3e9ffda8081870c8fe88d1d4c61f1b57495825d194c62c790698e4d0c00d44a2afffe4cffe06e5b7fe99a65127fbbd2989266d37b2b494f0c1e337c732c74d5
6
+ metadata.gz: 7010f9de4eb7b82981659da7e3a39896a03a88fb94aa6781e2e2379cda802568626c210b303e71541fc0b9b907e42d4df29253b12da9482902f1b786b531c402
7
+ data.tar.gz: 42a5c84dc33246476af82617e1629c10de17caf44175ecc33afbda90f8991d5ae16534dab195bf7154f32fa40f43cecb562ceffddc0564e6ed991d8905aca470
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.5.16.pre.pre8)
37
+ spree_cm_commissioner (2.5.16)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
data/README.md CHANGED
@@ -153,8 +153,37 @@ PIN_CODE_DEBUG_NOTIFIY_TELEGRAM_ENABLE="yes"
153
153
  EXCEPTION_NOTIFY_ENABLE="yes" # yes or no
154
154
  EXCEPTION_TELEGRAM_BOT_TOKEN=""
155
155
  EXCEPTION_NOTIFIER_TELEGRAM_CHANNEL_ID=""
156
+
157
+ TURNSTILE_SECRET_KEY="" # Cloudflare Turnstile secret key (server-side verification)
156
158
  ```
157
159
 
160
+ ### Cloudflare Turnstile
161
+
162
+ Cloudflare Turnstile is used to protect sensitive API endpoints from bots. The server-side validation is handled by `SpreeCmCommissioner::TurnstileTokenValidator` and the concern `SpreeCmCommissioner::TurnstileProtectable`.
163
+
164
+ **Protected endpoints:**
165
+
166
+ - `POST /api/v2/storefront/pin_code_generators`
167
+ - `POST /api/v2/storefront/pin_code_otp_generators`
168
+ - `POST /api/v2/storefront/user_registration_with_pin_codes`
169
+ - `PUT /api/v2/storefront/reset_passwords`
170
+ - `PATCH /api/v2/storefront/checkout/complete`
171
+
172
+ **How it works:**
173
+
174
+ 1. The client sends a Turnstile token via the `X-Turnstile-Token` HTTP header
175
+ 2. `TurnstileProtectable` reads the header and calls `TurnstileTokenValidator`
176
+ 3. The validator POSTs to Cloudflare's siteverify API with the token and `TURNSTILE_SECRET_KEY`
177
+ 4. Returns 403 if verification fails
178
+
179
+ **Configuration:**
180
+
181
+ 1. Set `TURNSTILE_SECRET_KEY` in `.env` (obtained from [Cloudflare Turnstile dashboard](https://dash.cloudflare.com/))
182
+ 2. For local development testing, use Cloudflare's test keys:
183
+ - Always pass: `1x0000000000000000000000000000000AA`
184
+ - Always fail: `2x0000000000000000000000000000000AA`
185
+ - Token spent: `3x0000000000000000000000000000000AA`
186
+
158
187
  ## JSON Format Examples For Store and Tenant Preferences
159
188
 
160
189
  ### Asset Links Format
@@ -0,0 +1,42 @@
1
+ module SpreeCmCommissioner
2
+ module TurnstileProtectable
3
+ extend ActiveSupport::Concern
4
+
5
+ private
6
+
7
+ # Verify the Turnstile token from the X-Turnstile-Token header.
8
+ # Renders 403 and halts the request if verification fails.
9
+ #
10
+ # Usage:
11
+ # before_action -> { verify_turnstile! }, only: :create
12
+ #
13
+ def verify_turnstile!
14
+ token = request.headers['X-Turnstile-Token']
15
+
16
+ # Skip verification if no token and Turnstile is not enforced
17
+ return if token.blank? && turnstile_optional?
18
+
19
+ result = TurnstileTokenValidator.call(
20
+ token: token,
21
+ remote_ip: request.remote_ip
22
+ )
23
+
24
+ return unless result.failure?
25
+
26
+ render json: {
27
+ errors: [{
28
+ status: '403',
29
+ title: 'Forbidden',
30
+ detail: result.message
31
+ }
32
+ ]
33
+ }, status: :forbidden
34
+ end
35
+
36
+ # Override in controllers to make Turnstile optional for specific actions.
37
+ # Default: required (returns false).
38
+ def turnstile_optional?
39
+ false
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,91 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Organizer
5
+ class InviteGuestsController < ::Spree::Api::V2::Organizer::BaseController
6
+ before_action :load_invite_guest_by_token, only: :show
7
+ before_action :load_invite_guest, :assign_line_item_data, only: :update
8
+
9
+ def show
10
+ render_serialized_payload { serialize_resource(@invite_guest) }
11
+ end
12
+
13
+ def update
14
+ return render_error(:revoked) if @invite_guest.revoked?
15
+ return render_error(:closed) if @invite_guest.expired?
16
+ return render_error(:fully_claimed) if @invite_guest.fully_claimed?
17
+
18
+ guest = @line_item.guests.new(guest_params)
19
+ guest.event_id = @invite_guest.event_id
20
+
21
+ if guest.save
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) if guest.event.vendor.preferred_telegram_chat_id.present?
24
+
25
+ render json: SpreeCmCommissioner::V2::Storefront::GuestSerializer.new(guest.reload).serializable_hash
26
+ else
27
+ render_error_payload(guest.errors.full_messages.to_sentence)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def send_guest_claimed_invitation_telegram_alert_to_vendor(guest)
34
+ title = '📣 --- [NEW GUEST CLAIMED INVITATION] ---' # Style/StringLiterals
35
+ chat_id = guest.event.vendor.preferred_telegram_chat_id
36
+ return if chat_id.blank?
37
+
38
+ factory = SpreeCmCommissioner::InviteGuestClaimedTelegramMessageFactory.new(
39
+ title: title,
40
+ order: @invite_guest.order,
41
+ guest: guest,
42
+ vendor: guest.event.vendor
43
+ )
44
+ SpreeCmCommissioner::TelegramNotificationSenderJob.perform_later(
45
+ chat_id: chat_id,
46
+ message: factory.message,
47
+ parse_mode: factory.parse_mode
48
+ )
49
+ end
50
+
51
+ def load_invite_guest_by_token
52
+ @invite_guest = SpreeCmCommissioner::InviteGuest.find_by(token: params[:id])
53
+ rescue ActiveRecord::RecordNotFound
54
+ render_error_payload(I18n.t('invite.url_not_found'))
55
+ end
56
+
57
+ def load_invite_guest
58
+ @invite_guest = SpreeCmCommissioner::InviteGuest.find(params[:id])
59
+ end
60
+
61
+ def assign_line_item_data
62
+ @line_item = @invite_guest.order.line_items.first
63
+ @guests = @line_item.guests
64
+ end
65
+
66
+ def render_error(message)
67
+ render json: { errors: message }
68
+ end
69
+
70
+ def guest_params
71
+ params.require(:invite_guest).permit(
72
+ :first_name,
73
+ :last_name,
74
+ :dob,
75
+ :gender,
76
+ :age,
77
+ :emergency_contact,
78
+ :phone_number,
79
+ :address,
80
+ :other_organization
81
+ )
82
+ end
83
+
84
+ def resource_serializer
85
+ ::Spree::V2::Organizer::InviteGuestSerializer
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,77 @@
1
+ module Spree
2
+ module Api
3
+ module V2
4
+ module Storefront
5
+ class SavedGuestsController < ::Spree::Api::V2::ResourceController
6
+ before_action :require_spree_current_user
7
+ before_action :load_saved_guest, only: %i[show update destroy]
8
+
9
+ def collection
10
+ spree_current_user.saved_guests.order(created_at: :desc)
11
+ .page(params[:page])
12
+ .per(params[:per_page])
13
+ end
14
+
15
+ def create
16
+ saved_guest = spree_current_user.saved_guests.new(saved_guest_params)
17
+
18
+ if saved_guest.save
19
+ render_serialized_payload(201) { serialize_resource(saved_guest) }
20
+ else
21
+ render_error_payload(saved_guest.errors.full_messages.to_sentence, 422)
22
+ end
23
+ end
24
+
25
+ def update
26
+ if @saved_guest.update(saved_guest_params)
27
+ render_serialized_payload { serialize_resource(@saved_guest) }
28
+ else
29
+ render_error_payload(@saved_guest.errors.full_messages.to_sentence, 400)
30
+ end
31
+ end
32
+
33
+ def destroy
34
+ @saved_guest.destroy
35
+ head :no_content
36
+ end
37
+
38
+ private
39
+
40
+ def model_class
41
+ SpreeCmCommissioner::SavedGuest
42
+ end
43
+
44
+ def collection_serializer
45
+ resource_serializer
46
+ end
47
+
48
+ def resource_serializer
49
+ SpreeCmCommissioner::V2::Storefront::SavedGuestSerializer
50
+ end
51
+
52
+ def load_saved_guest
53
+ @saved_guest = spree_current_user.saved_guests.find(params[:id])
54
+ end
55
+
56
+ def saved_guest_params
57
+ params.require(:saved_guest).permit(
58
+ :first_name,
59
+ :last_name,
60
+ :dob,
61
+ :age,
62
+ :gender,
63
+ :email,
64
+ :country_code,
65
+ :phone_number,
66
+ :intel_phone_number,
67
+ :nationality_id,
68
+ :occupation_id,
69
+ :nationality_group,
70
+ :age_group
71
+ )
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -7,34 +7,6 @@ module SpreeCmCommissioner
7
7
  formatted_time = Time.zone.parse(time.to_s)
8
8
  timezone ? formatted_time.in_time_zone(timezone) : formatted_time
9
9
  end
10
-
11
- # Helper to parse date strings or return date objects
12
- def parse_date(date_obj)
13
- return date_obj if date_obj.is_a?(Date)
14
-
15
- Date.parse(date_obj.to_s)
16
- rescue ArgumentError
17
- nil
18
- end
19
-
20
- def normalize_date(value)
21
- if value.respond_to?(:to_date) && value.to_date == Time.zone.now.to_date
22
- Time.zone.now
23
- else
24
- Time.zone.parse(value.to_s)
25
- end
26
- end
27
-
28
- def minutes_since_midnight(value)
29
- case value
30
- when Time, ActiveSupport::TimeWithZone
31
- (value.hour * 60) + value.min
32
- else
33
- str = value.to_s.strip
34
- h, m = str.split(':', 2).map(&:to_i)
35
- (h * 60) + (m || 0)
36
- end
37
- end
38
10
  end
39
11
  end
40
12
  end
@@ -11,6 +11,8 @@ module SpreeCmCommissioner
11
11
  create_logo
12
12
  create_role
13
13
  end
14
+
15
+ context.vendor = @vendor
14
16
  end
15
17
 
16
18
  private
@@ -0,0 +1,55 @@
1
+ module SpreeCmCommissioner
2
+ class TurnstileTokenValidator < BaseInteractor
3
+ SITE_VERIFY_URL = 'https://challenges.cloudflare.com/turnstile/v0/siteverify'.freeze
4
+
5
+ delegate :token, :remote_ip, to: :context
6
+
7
+ def call
8
+ context.fail!(message: I18n.t('turnstile.token_missing')) if token.blank?
9
+
10
+ response = verify_token
11
+ body = JSON.parse(response.body)
12
+
13
+ unless body['success']
14
+ error_codes = body['error-codes']&.join(', ') || 'unknown'
15
+ Rails.logger.warn(
16
+ "Turnstile verification failed: #{error_codes} " \
17
+ "from IP #{remote_ip} | hostname=#{body['hostname']}"
18
+ )
19
+ context.fail!(message: I18n.t('turnstile.verification_failed'))
20
+ end
21
+
22
+ context.challenge_ts = body['challenge_ts']
23
+ context.hostname = body['hostname']
24
+ context.action = body['action']
25
+ end
26
+
27
+ private
28
+
29
+ def verify_token
30
+ connection.post do |req|
31
+ req.body = {
32
+ secret: secret_key,
33
+ response: token,
34
+ remoteip: remote_ip
35
+ }
36
+ end
37
+ rescue Faraday::TimeoutError, Faraday::ConnectionFailed => e
38
+ Rails.logger.error("Turnstile API error: #{e.class} - #{e.message}")
39
+ context.fail!(message: I18n.t('turnstile.service_unavailable'))
40
+ end
41
+
42
+ def connection
43
+ @connection ||= Faraday.new(url: SITE_VERIFY_URL) do |f|
44
+ f.request :url_encoded
45
+ f.options.open_timeout = 3
46
+ f.options.timeout = 5
47
+ f.adapter Faraday.default_adapter
48
+ end
49
+ end
50
+
51
+ def secret_key
52
+ ENV.fetch('TURNSTILE_SECRET_KEY')
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,9 @@
1
+ module SpreeCmCommissioner
2
+ class CleanupExpiredAccessTokensJob < SpreeCmCommissioner::ApplicationJob
3
+ queue_as :default
4
+
5
+ def perform
6
+ SpreeCmCommissioner::CleanupExpiredAccessTokens.new.call
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ module SpreeCmCommissioner
2
+ class Agency < Base
3
+ belongs_to :vendor, class_name: 'Spree::Vendor', optional: false
4
+ belongs_to :agency_category, class_name: 'Spree::Taxon', optional: false
5
+
6
+ enum approval_status: { pending: 0, approved: 1, rejected: 2 }
7
+ enum status: { inactive: 0, active: 1 }
8
+ enum paid_type: { postpaid: 0, prepaid: 1 }
9
+
10
+ validates :name, presence: true
11
+ end
12
+ end
@@ -1,6 +1,8 @@
1
1
  module SpreeCmCommissioner
2
2
  module RoleDecorator
3
3
  def self.prepended(base)
4
+ base.enum role_type: { internal: 0, external: 1 }
5
+
4
6
  base.has_many :role_permissions, class_name: 'SpreeCmCommissioner::RolePermission'
5
7
  base.has_many :permissions, through: :role_permissions, class_name: 'SpreeCmCommissioner::Permission'
6
8
 
@@ -10,6 +12,7 @@ module SpreeCmCommissioner
10
12
  base.scope :filter_by_vendor, lambda { |vendor|
11
13
  where(vendor_id: vendor)
12
14
  }
15
+ base.scope :filter_external, -> { where(role_type: :external) }
13
16
 
14
17
  base.accepts_nested_attributes_for :role_permissions, allow_destroy: true
15
18
 
@@ -5,8 +5,6 @@ module SpreeCmCommissioner
5
5
 
6
6
  attr_accessor :hours, :minutes, :seconds
7
7
 
8
- enum trip_type: { direct: 0, multi_leg: 1 }
9
-
10
8
  # This model has no seat_layout column (polymorphic association), so we store the preload_seat_layout_id ID in public_metadata.
11
9
  # This lets us check if a seat layout exists without triggering a database query.
12
10
  # The ID is automatically updated whenever the seat_layout is saved.
@@ -24,6 +24,7 @@ module SpreeCmCommissioner
24
24
  base.has_many :user_events, class_name: 'SpreeCmCommissioner::UserEvent'
25
25
  base.has_many :events, through: :user_events, class_name: 'Spree::Taxon', source: 'taxon'
26
26
  base.has_many :guests, class_name: 'SpreeCmCommissioner::Guest', dependent: :destroy
27
+ base.has_many :saved_guests, class_name: 'SpreeCmCommissioner::SavedGuest', dependent: :nullify
27
28
 
28
29
  base.has_many :google_user_identity_providers,
29
30
  -> { where(identity_type: :google) },
@@ -16,39 +16,139 @@ module SpreeCmCommissioner
16
16
  end
17
17
 
18
18
  def call
19
- return Kaminari.paginate_array([]).page(@page).per(@per_page) if date.to_date < Date.current
19
+ return Kaminari.paginate_array([]) if date.to_date < Date.current
20
20
 
21
- result = direct_trips
22
- result += multi_leg_trips if result.empty? || result.size < 3 || @params[:include_multi_leg] == true
23
- Kaminari.paginate_array(result).page(@page).per(@per_page)
21
+ paginated_relation = direct_trips
22
+ return Kaminari.paginate_array([]) if paginated_relation.empty?
23
+
24
+ unique_trips = paginated_relation.uniq(&:id)
25
+ results_array = unique_trips.map do |trip|
26
+ result = build_trip_result(trip)
27
+ SpreeCmCommissioner::TripQueryResult.new([result])
28
+ end
29
+
30
+ Kaminari.paginate_array(
31
+ results_array,
32
+ total_count: unique_trips.size,
33
+ limit: unique_trips.size,
34
+ offset: paginated_relation.offset_value
35
+ )
36
+ end
37
+
38
+ def direct_trips
39
+ result = trip_scope
40
+ result = result.where({ vendor_id: vendor_id }.compact) if vendor_id.present?
41
+ result = result.where(route_type: route_type) if route_type.present?
42
+ result = result.where(spree_vendors: { tenant_id: tenant_id }) if tenant_id.present?
43
+ paginate(result)
44
+ end
45
+
46
+ def product_inventory_totals
47
+ Spree::Product
48
+ .select(
49
+ 'spree_products.id AS product_id,
50
+ SUM(cm_inventory_items.max_capacity) AS max_capacity,
51
+ SUM(cm_inventory_items.quantity_available) AS quantity_available'
52
+ )
53
+ .joins(variants: :inventory_items)
54
+ .where('cm_inventory_items.inventory_date = ? AND cm_inventory_items.quantity_available >= ?', @date.to_date, @number_of_guests)
55
+ .group('spree_products.id')
24
56
  end
25
57
 
26
58
  private
27
59
 
28
- def direct_trips
29
- SpreeCmCommissioner::SingleLegTripsQuery.new(
30
- origin_id: origin_id,
31
- destination_id: destination_id,
32
- date: date,
33
- route_type: route_type,
34
- vendor_id: vendor_id,
35
- tenant_id: tenant_id,
36
- number_of_guests: number_of_guests,
37
- params: params
38
- ).call
39
- end
40
-
41
- def multi_leg_trips
42
- SpreeCmCommissioner::MultiLegTripsQuery.new(
43
- origin_id: origin_id,
44
- destination_id: destination_id,
45
- date: date,
46
- route_type: route_type,
47
- vendor_id: vendor_id,
48
- tenant_id: tenant_id,
49
- number_of_guests: number_of_guests,
50
- params: params
51
- ).call
60
+ def trip_scope
61
+ scope = SpreeCmCommissioner::Trip
62
+ .select(<<~SQL.squish)
63
+ cm_trips.*,
64
+ boarding.departure_time AS boarding_departure_time,
65
+ boarding.stop_place_id AS boarding_stop_id, boarding.stop_name AS boarding_stop_name,
66
+ drop_off.stop_name AS drop_off_stop_name, drop_off.stop_place_id AS drop_off_stop_id,
67
+ drop_off.arrival_time AS drop_off_arrival_time,
68
+ COALESCE(iv.quantity_available, 0) AS quantity_available,
69
+ COALESCE(iv.max_capacity, 0) AS max_capacity,
70
+ origin_places.name AS origin_place_name, dest_places.name AS destination_place_name,
71
+ prices.amount AS amount, prices.compare_at_amount AS compare_at_amount,
72
+ prices.currency AS currency
73
+ SQL
74
+ .includes(vendor: :logo, vehicle_type: :option_values, route: {}, open_dated_product: %i[trip variants])
75
+
76
+ scope = scope.joins(:vendor) if tenant_id.present?
77
+
78
+ scope
79
+ .joins(<<~SQL.squish)
80
+ INNER JOIN cm_trip_stops AS boarding ON boarding.trip_id = cm_trips.id AND boarding.allow_boarding = true
81
+ INNER JOIN cm_trip_stops AS drop_off ON drop_off.trip_id = cm_trips.id AND drop_off.allow_drop_off = true
82
+ INNER JOIN cm_places AS origin_places ON origin_places.id = cm_trips.origin_place_id
83
+ INNER JOIN cm_places AS dest_places ON dest_places.id = cm_trips.destination_place_id
84
+ INNER JOIN spree_variants AS master ON master.product_id = cm_trips.product_id AND master.is_master = true
85
+ INNER JOIN spree_prices AS prices ON prices.variant_id = master.id AND prices.deleted_at IS NULL
86
+ SQL
87
+ .where('(boarding.location_place_id = ? OR boarding.stop_place_id = ? OR cm_trips.origin_place_id = ?)
88
+ AND (drop_off.location_place_id = ? OR drop_off.stop_place_id = ? OR cm_trips.destination_place_id = ?)',
89
+ origin_id, origin_id, origin_id, destination_id, destination_id, destination_id
90
+ )
91
+ .joins("INNER JOIN (#{inventory_sql.to_sql}) iv ON cm_trips.product_id = iv.product_id")
92
+ end
93
+
94
+ def paginate(result)
95
+ @per_page.to_i.positive? ? result.page(@page).per(@per_page) : result.page(@page)
96
+ end
97
+
98
+ def inventory_sql
99
+ @inventory_sql ||= product_inventory_totals
100
+ end
101
+
102
+ def build_trip_result(trip)
103
+ vehicle_type = trip&.vehicle_type
104
+ vendor = trip&.vendor
105
+ trip_result_options = {
106
+ id: trip&.id,
107
+ departure_time: trip&.departure_time,
108
+ duration: trip&.duration,
109
+ distance: trip&.distance,
110
+ allow_seat_selection: trip&.allow_seat_selection,
111
+ route_type: trip&.route_type,
112
+ origin_place: {
113
+ id: trip&.origin_place_id,
114
+ name: trip&.origin_place_name
115
+ },
116
+ destination_place: {
117
+ id: trip&.destination_place_id,
118
+ name: trip&.destination_place_name
119
+ },
120
+ vehicle_type_id: trip&.vehicle_type_id,
121
+ vehicle_type: vehicle_type,
122
+ vendor_id: trip&.vendor_id,
123
+ vendor: vendor,
124
+ product_id: trip&.product_id,
125
+ price: trip&.amount,
126
+ currency: trip&.currency,
127
+ compare_at_amount: trip&.compare_at_amount,
128
+ boarding: build_boarding_info(trip),
129
+ drop_off: build_drop_off_info(trip),
130
+ quantity_available: trip&.quantity_available,
131
+ max_capacity: trip&.max_capacity,
132
+ amenities: (trip.vehicle_type&.option_values || []),
133
+ open_dated_product: trip&.open_dated_product
134
+ }
135
+ SpreeCmCommissioner::TripResult.new(trip_result_options)
136
+ end
137
+
138
+ def build_boarding_info(trip)
139
+ {
140
+ stop_id: trip&.boarding_stop_id,
141
+ stop_name: trip&.boarding_stop_name,
142
+ departure_time: trip&.boarding_departure_time
143
+ }
144
+ end
145
+
146
+ def build_drop_off_info(trip)
147
+ {
148
+ stop_id: trip&.drop_off_stop_id,
149
+ stop_name: trip&.drop_off_stop_name,
150
+ arrival_time: trip&.drop_off_arrival_time
151
+ }
52
152
  end
53
153
  end
54
154
  end
@@ -0,0 +1,13 @@
1
+ module Spree
2
+ module V2
3
+ module Organizer
4
+ class InviteGuestSerializer < BaseSerializer
5
+ attributes :email, :quantity, :token, :invite_type, :claimed_status, :issued_to, :expiration_date,
6
+ :email_send_at, :created_at, :updated_at, :remark
7
+ belongs_to :variant, serializer: SpreeCmCommissioner::V2::Storefront::EventVariantSerializer
8
+ belongs_to :order, serializer: Spree::V2::Storefront::OrderSerializer
9
+ belongs_to :event, serializer: Spree::V2::Storefront::TaxonSerializer
10
+ end
11
+ end
12
+ end
13
+ end
@@ -11,7 +11,7 @@ module Spree
11
11
  :display_amount, :number, :qr_data, :kyc, :kyc_fields, :remaining_total_guests, :number_of_guests,
12
12
  :completion_steps, :available_social_contact_platforms, :allow_anonymous_booking,
13
13
  :discontinue_on, :high_demand, :jwt_token, :pre_tax_amount, :display_pre_tax_amount, :public_metadata,
14
- :direction, :trip_id, :boarding_trip_stop_id, :drop_off_trip_stop_id
14
+ :direction, :trip_id, :boarding_trip_stop_id, :drop_off_trip_stop_id, :passenger_count, :remark
15
15
 
16
16
  attribute :required_self_check_in_location, &:required_self_check_in_location?
17
17
  attribute :allowed_self_check_in, &:allowed_self_check_in?
@@ -0,0 +1,52 @@
1
+ module SpreeCmCommissioner
2
+ class CleanupExpiredAccessTokens
3
+ prepend ::Spree::ServiceModule::Base
4
+
5
+ BATCH_SIZE = 1000
6
+ EXPIRATION_THRESHOLD_DAYS = 1
7
+
8
+ def call
9
+ cutoff_date = EXPIRATION_THRESHOLD_DAYS.days.ago
10
+ total_deleted = 0
11
+
12
+ Spree::OauthAccessToken
13
+ .where.not(expires_in: nil)
14
+ .where('created_at + make_interval(secs => expires_in) < ?', cutoff_date)
15
+ .in_batches(of: BATCH_SIZE) do |relation|
16
+ deleted_count = relation.delete_all
17
+ total_deleted += deleted_count
18
+ end
19
+
20
+ log_cleanup_result(total_deleted, cutoff_date)
21
+ success(total_deleted: total_deleted, cutoff_date: cutoff_date, batch_size: BATCH_SIZE, expiration_threshold_days: EXPIRATION_THRESHOLD_DAYS)
22
+ rescue StandardError => e
23
+ log_error(e)
24
+ failure(nil, e.message)
25
+ end
26
+
27
+ private
28
+
29
+ def log_cleanup_result(total_deleted, cutoff_date)
30
+ CmAppLogger.log(
31
+ label: 'SpreeCmCommissioner::CleanupExpiredAccessTokens completed',
32
+ data: {
33
+ total_deleted: total_deleted,
34
+ cutoff_date: cutoff_date,
35
+ batch_size: BATCH_SIZE,
36
+ expiration_threshold_days: EXPIRATION_THRESHOLD_DAYS
37
+ }
38
+ )
39
+ end
40
+
41
+ def log_error(error)
42
+ CmAppLogger.error(
43
+ label: 'SpreeCmCommissioner::CleanupExpiredAccessTokens error',
44
+ data: {
45
+ error_class: error.class.name,
46
+ error_message: error.message,
47
+ backtrace: error.backtrace&.first(5)&.join("\n")
48
+ }
49
+ )
50
+ end
51
+ end
52
+ end