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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +29 -0
- data/app/controllers/concerns/spree_cm_commissioner/turnstile_protectable.rb +42 -0
- data/app/controllers/spree/api/v2/organizer/invite_guests_controller.rb +91 -0
- data/app/controllers/spree/api/v2/storefront/saved_guests_controller.rb +77 -0
- data/app/helpers/spree_cm_commissioner/transit/trip_helper.rb +0 -28
- data/app/interactors/spree_cm_commissioner/create_vendor.rb +2 -0
- data/app/interactors/spree_cm_commissioner/turnstile_token_validator.rb +55 -0
- data/app/jobs/spree_cm_commissioner/cleanup_expired_access_tokens_job.rb +9 -0
- data/app/models/spree_cm_commissioner/agency.rb +12 -0
- data/app/models/spree_cm_commissioner/role_decorator.rb +3 -0
- data/app/models/spree_cm_commissioner/trip.rb +0 -2
- data/app/models/spree_cm_commissioner/user_decorator.rb +1 -0
- data/app/queries/spree_cm_commissioner/trip_query.rb +128 -28
- data/app/serializers/spree/v2/organizer/invite_guest_serializer.rb +13 -0
- data/app/serializers/spree/v2/tenant/line_item_serializer.rb +1 -1
- data/app/services/spree_cm_commissioner/cleanup_expired_access_tokens.rb +52 -0
- data/app/services/spree_cm_commissioner/transit_order/create.rb +6 -48
- data/app/services/spree_cm_commissioner/trips/create_single_leg.rb +1 -2
- data/app/services/spree_cm_commissioner/trips/search.rb +8 -12
- data/config/locales/en.yml +5 -0
- data/config/locales/km.yml +5 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20260318081516_create_cm_agencies.rb +18 -0
- data/db/migrate/20260319090000_add_role_type_to_spree_roles.rb +5 -0
- data/db/migrate/20260320103313_add_index_to_spree_oauth_access_tokens_created_at.rb +7 -0
- data/lib/spree_cm_commissioner/transit/service_calendar_form.rb +0 -19
- data/lib/spree_cm_commissioner/trip_query_result.rb +6 -30
- data/lib/spree_cm_commissioner/trip_result.rb +0 -55
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +15 -9
- data/app/queries/spree_cm_commissioner/multi_leg_trips_query.rb +0 -292
- data/app/queries/spree_cm_commissioner/single_leg_trips_query.rb +0 -91
- data/app/services/spree_cm_commissioner/trips/create_multi_leg.rb +0 -542
- data/app/services/spree_cm_commissioner/trips/preload_inventory.rb +0 -80
- 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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b8c433f50fb2876095dad4e8adbf87547a04c0bdef08f1db15f545a5f3539aa2
|
|
4
|
+
data.tar.gz: 3af1f877767be983922d340cd0249c3bfbf897e6e544ffaae3ea2bb37e00bd82
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7010f9de4eb7b82981659da7e3a39896a03a88fb94aa6781e2e2379cda802568626c210b303e71541fc0b9b907e42d4df29253b12da9482902f1b786b531c402
|
|
7
|
+
data.tar.gz: 42a5c84dc33246476af82617e1629c10de17caf44175ecc33afbda90f8991d5ae16534dab195bf7154f32fa40f43cecb562ceffddc0564e6ed991d8905aca470
|
data/Gemfile.lock
CHANGED
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
|
|
@@ -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,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([])
|
|
19
|
+
return Kaminari.paginate_array([]) if date.to_date < Date.current
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
29
|
-
SpreeCmCommissioner::
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|