spree_cm_commissioner 2.2.1 → 2.3.0.pre.pre2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/app/controllers/spree/api/v2/tenant/user_account_linkages_controller.rb +5 -1
- data/app/errors/spree_cm_commissioner/seats/blocks_are_on_hold_by_other_guest_error.rb +8 -0
- data/app/errors/spree_cm_commissioner/seats/blocks_are_reserved_by_other_guest_error.rb +8 -0
- data/app/errors/spree_cm_commissioner/seats/blocks_are_reserved_by_same_guest_error.rb +8 -0
- data/app/errors/spree_cm_commissioner/seats/unable_to_save_reserved_block_record_error.rb +8 -0
- data/app/helpers/spree/base_helper_decorator.rb +1 -1
- data/app/interactors/spree_cm_commissioner/account_linkage.rb +28 -5
- data/app/interactors/spree_cm_commissioner/create_ticket.rb +6 -4
- data/app/interactors/spree_cm_commissioner/inventory_item_syncer.rb +10 -0
- data/app/interactors/spree_cm_commissioner/order_importer/multi_guest.rb +9 -1
- data/app/interactors/spree_cm_commissioner/order_importer/single_guest.rb +9 -1
- data/app/interactors/spree_cm_commissioner/pin_code_creator.rb +9 -1
- data/app/mailers/spree/order_mailer_decorator.rb +26 -10
- data/app/models/concerns/spree_cm_commissioner/order_seatable.rb +36 -14
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +3 -2
- data/app/models/concerns/spree_cm_commissioner/service_recommendations.rb +62 -0
- data/app/models/concerns/spree_cm_commissioner/store_metadata.rb +26 -1
- data/app/models/spree_cm_commissioner/block.rb +1 -1
- data/app/models/spree_cm_commissioner/guest.rb +2 -2
- data/app/models/spree_cm_commissioner/inventory_item.rb +16 -8
- data/app/models/spree_cm_commissioner/oauth_application_decorator.rb +7 -1
- data/app/models/spree_cm_commissioner/order_decorator.rb +37 -1
- data/app/models/spree_cm_commissioner/product_decorator.rb +1 -0
- data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +23 -1
- data/app/models/spree_cm_commissioner/seats/blocks_canceler.rb +1 -1
- data/app/models/spree_cm_commissioner/seats/blocks_holder.rb +11 -5
- data/app/models/spree_cm_commissioner/seats/blocks_reserver.rb +4 -4
- data/app/models/spree_cm_commissioner/variant_decorator.rb +16 -0
- data/app/models/spree_cm_commissioner/vendor_decorator.rb +3 -0
- data/app/request_schemas/spree_cm_commissioner/user_account_linkage_request_schema.rb +15 -1
- data/app/serializers/spree/v2/storefront/cart_serializer_decorator.rb +1 -1
- data/app/services/spree_cm_commissioner/checkout/advance_decorator.rb +17 -0
- data/app/services/spree_cm_commissioner/imports/create_order_service.rb +14 -6
- data/app/views/spree/admin/stock_managements/index.html.erb +2 -2
- data/app/views/spree_cm_commissioner/order_mailer/_mailer_stylesheets.html.erb +16 -1
- data/app/views/spree_cm_commissioner/order_mailer/purchased_items/_items.html.erb +1 -1
- data/app/views/spree_cm_commissioner/order_mailer/tenant/_greeting.html.erb +1 -1
- data/app/views/spree_cm_commissioner/order_mailer/tenant/_support_contact.html.erb +2 -2
- data/config/locales/en.yml +4 -0
- data/config/locales/km.yml +4 -0
- data/config/routes.rb +0 -1
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/tasks/backfill_confirmed_at.rake +4 -7
- metadata +10 -9
- data/app/controllers/spree/api/v2/storefront/anonymous_line_items_controller.rb +0 -39
- data/app/models/spree_cm_commissioner/seats/errors/blocks_are_on_hold_by_other_guest.rb +0 -4
- data/app/models/spree_cm_commissioner/seats/errors/blocks_are_reserved_by_other_guest.rb +0 -4
- data/app/models/spree_cm_commissioner/seats/errors/blocks_are_reserved_by_same_guest.rb +0 -4
- data/app/models/spree_cm_commissioner/seats/errors/unable_to_save_reserved_block_record.rb +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f500a4db3e391dbe9f28f9bb99cdab6ffbe1164e266b726fc3c63f0c39d4afbc
|
|
4
|
+
data.tar.gz: cc21615f9b7cf21f802b3088089a463fd6dc69e30d85c989e71a48182fbcff3f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 48dcfc4d64219cf42e736f89c1138f7504e5aed6992daa8b773d38dadb1c68f0cb67b30849f7e2c93bff9d953297a7d4033fa53e114bbbc91289654a9bbbb4c4
|
|
7
|
+
data.tar.gz: ece3ecbeba612e1a7a07f5bc1033a43deaf3525f27d0878d7b8a8de025fe298390282e5177d58a5b8ffa4b564af99b8c26d7a31d73ff1282618c8b008c78ce3c
|
data/Gemfile.lock
CHANGED
|
@@ -12,7 +12,11 @@ module Spree
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def create
|
|
15
|
-
context = SpreeCmCommissioner::AccountLinkage.call(
|
|
15
|
+
context = SpreeCmCommissioner::AccountLinkage.call(
|
|
16
|
+
user: spree_current_user,
|
|
17
|
+
id_token: params[:id_token],
|
|
18
|
+
fb_access_token: params[:fb_access_token]
|
|
19
|
+
)
|
|
16
20
|
|
|
17
21
|
if context.success?
|
|
18
22
|
identity_provider = context.identity_provider
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
class AccountLinkage
|
|
3
3
|
include Interactor
|
|
4
|
+
delegate :id_token, :fb_access_token, :user, to: :context
|
|
4
5
|
|
|
5
6
|
# id_token:, user:
|
|
6
7
|
def call
|
|
7
|
-
|
|
8
|
+
# Prefer Firebase id_token if provided
|
|
9
|
+
if id_token.present?
|
|
10
|
+
handle_id_token
|
|
11
|
+
elsif fb_access_token.present?
|
|
12
|
+
handle_fb_access_token
|
|
13
|
+
else
|
|
14
|
+
context.fail!(message: 'missing_social_token')
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def handle_id_token
|
|
21
|
+
firebase_id_token_context = FirebaseIdTokenProvider.call(id_token: id_token)
|
|
8
22
|
|
|
9
23
|
if firebase_id_token_context.success?
|
|
10
24
|
build_user_identify_provider(firebase_id_token_context.provider)
|
|
@@ -13,7 +27,15 @@ module SpreeCmCommissioner
|
|
|
13
27
|
end
|
|
14
28
|
end
|
|
15
29
|
|
|
16
|
-
|
|
30
|
+
def handle_fb_access_token
|
|
31
|
+
facebook_context = FetchFacebookUserData.call(fb_access_token: fb_access_token)
|
|
32
|
+
|
|
33
|
+
if facebook_context.success?
|
|
34
|
+
build_user_identify_provider(facebook_context.provider)
|
|
35
|
+
else
|
|
36
|
+
context.fail!(message: facebook_context.message)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
17
39
|
|
|
18
40
|
# {
|
|
19
41
|
# provider_name: provider_name,
|
|
@@ -21,9 +43,10 @@ module SpreeCmCommissioner
|
|
|
21
43
|
# sub: sub
|
|
22
44
|
# }
|
|
23
45
|
def build_user_identify_provider(provider)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
46
|
+
identity_type_value = provider[:identity_type]
|
|
47
|
+
# Firebase provider gives a String (e.g., 'google', 'apple'),
|
|
48
|
+
# while Facebook interactor provides the enum Integer directly.
|
|
49
|
+
identity_type = identity_type_value.is_a?(String) ? UserIdentityProvider.identity_types[identity_type_value] : identity_type_value
|
|
27
50
|
|
|
28
51
|
identity_provider = UserIdentityProvider
|
|
29
52
|
.where(user_id: context.user, identity_type: identity_type)
|
|
@@ -68,14 +68,16 @@ module SpreeCmCommissioner
|
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def create_stock_item
|
|
71
|
-
|
|
71
|
+
stock_movement_context = SpreeCmCommissioner::Stock::StockMovementCreator.call(
|
|
72
72
|
variant_id: @variant.id,
|
|
73
73
|
stock_location_id: context.params[:stock_location_id],
|
|
74
|
-
|
|
74
|
+
current_store: @store,
|
|
75
|
+
stock_movement_params: { quantity: context.params[:count_on_hand] }
|
|
75
76
|
)
|
|
76
|
-
return if @stock_item.save
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
return if stock_movement_context.success?
|
|
79
|
+
|
|
80
|
+
context.fail!(message: stock_movement_context.message)
|
|
79
81
|
end
|
|
80
82
|
|
|
81
83
|
def upload_image_to(target)
|
|
@@ -15,6 +15,16 @@ module SpreeCmCommissioner
|
|
|
15
15
|
private
|
|
16
16
|
|
|
17
17
|
def adjust_quantity_available(inventory_item, quantity)
|
|
18
|
+
# IMPORTANT: Apply the quantity change directly without defensive clamping.
|
|
19
|
+
# The model validation will catch any attempts to go negative, surfacing bugs
|
|
20
|
+
# in upstream Redis deduction logic.
|
|
21
|
+
#
|
|
22
|
+
# ❌ DO NOT use defensive clamping like:
|
|
23
|
+
# new_quantity = [inventory_item.quantity_available + quantity, 0].max
|
|
24
|
+
#
|
|
25
|
+
# Why? Clamping masks bugs in Redis deduction. If Redis deducted 5 but this
|
|
26
|
+
# job tries to deduct 10, clamping silently loses the 5-unit discrepancy.
|
|
27
|
+
# Validation errors are better than silent data loss.
|
|
18
28
|
inventory_item.update!(quantity_available: inventory_item.quantity_available + quantity)
|
|
19
29
|
end
|
|
20
30
|
|
|
@@ -8,7 +8,15 @@ module SpreeCmCommissioner
|
|
|
8
8
|
return context.fail!(message: 'email_or_phone_is_required') if params[:order_email].blank? && params[:order_phone_number].blank?
|
|
9
9
|
|
|
10
10
|
context.order = construct_order
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
begin
|
|
13
|
+
# Wraps order save and inventory update in a transaction to ensure atomicity.
|
|
14
|
+
# If either the order save or inventory update fails, both operations will be rolled back.
|
|
15
|
+
# This prevents partial updates that could lead to data inconsistency.
|
|
16
|
+
context.order.unstock_inventory! { context.order.save! }
|
|
17
|
+
rescue StandardError => e
|
|
18
|
+
context.fail!(message: "Failed to save order: #{e.message}")
|
|
19
|
+
end
|
|
12
20
|
end
|
|
13
21
|
|
|
14
22
|
def construct_order
|
|
@@ -8,7 +8,15 @@ module SpreeCmCommissioner
|
|
|
8
8
|
return context.fail!(message: 'email_or_phone_is_required') if params[:order_email].blank? && params[:order_phone_number].blank?
|
|
9
9
|
|
|
10
10
|
context.order = construct_order
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
begin
|
|
13
|
+
# Wraps order save and inventory update in a transaction to ensure atomicity.
|
|
14
|
+
# If either the order save or inventory update fails, both operations will be rolled back.
|
|
15
|
+
# This prevents partial updates that could lead to data inconsistency.
|
|
16
|
+
context.order.unstock_inventory! { context.order.save! }
|
|
17
|
+
rescue StandardError => e
|
|
18
|
+
context.fail!(message: "Failed to save order: #{e.message}")
|
|
19
|
+
end
|
|
12
20
|
end
|
|
13
21
|
|
|
14
22
|
def construct_order
|
|
@@ -4,7 +4,15 @@ module SpreeCmCommissioner
|
|
|
4
4
|
set_contact
|
|
5
5
|
set_application
|
|
6
6
|
|
|
7
|
-
attrs = { contact: context.contact,
|
|
7
|
+
attrs = { contact: context.contact,
|
|
8
|
+
contact_type: context.contact_type,
|
|
9
|
+
type: context.type
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
# Only set application if cm_pin_codes.application_id exists (some test schemas may not include it)
|
|
13
|
+
if SpreeCmCommissioner::PinCode.column_names.include?('application_id') && context.application.present?
|
|
14
|
+
attrs[:application] = context.application
|
|
15
|
+
end
|
|
8
16
|
new_pin_code = SpreeCmCommissioner::PinCode.new(attrs)
|
|
9
17
|
|
|
10
18
|
if new_pin_code.save
|
|
@@ -11,19 +11,15 @@ module Spree
|
|
|
11
11
|
@order = order.respond_to?(:id) ? order : Spree::Order.find(order)
|
|
12
12
|
return false if @order.email.blank?
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
if @tenant.present?
|
|
16
|
-
@brand_color = @tenant.preferences[:brand_primary_color]
|
|
17
|
-
@vendor_logo_url = @tenant.active_vendor&.logo&.original_url
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
@current_store = @order.store
|
|
14
|
+
setup_tenant_and_store
|
|
21
15
|
@product_type = @order.products.first&.product_type || 'accommodation'
|
|
22
16
|
|
|
23
|
-
subject =
|
|
24
|
-
subject += "#{@current_store&.name} Booking Confirmation ##{@order.number}"
|
|
17
|
+
subject = build_subject(resend)
|
|
25
18
|
|
|
26
|
-
mail(to: @order.email,
|
|
19
|
+
mail(to: @order.email,
|
|
20
|
+
from: from_email_address, subject: subject,
|
|
21
|
+
store_url: store_url
|
|
22
|
+
) do |format|
|
|
27
23
|
format.html { render layout: 'spree_cm_commissioner/layouts/order_mailer' }
|
|
28
24
|
format.text
|
|
29
25
|
end
|
|
@@ -39,6 +35,26 @@ module Spree
|
|
|
39
35
|
end
|
|
40
36
|
end
|
|
41
37
|
|
|
38
|
+
def setup_tenant_and_store
|
|
39
|
+
@tenant = @order.tenant
|
|
40
|
+
if @tenant.present?
|
|
41
|
+
@brand_color = @tenant.preferences[:brand_primary_color]
|
|
42
|
+
@vendor_logo_url = @tenant.active_vendor&.logo&.original_url
|
|
43
|
+
@current_store = @tenant
|
|
44
|
+
else
|
|
45
|
+
@current_store = @order.store
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def build_subject(resend)
|
|
50
|
+
prefix = resend ? "[#{Spree.t(:resend).upcase}] " : ''
|
|
51
|
+
"#{prefix}#{@current_store&.name} Booking Confirmation ##{@order.number}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def store_url
|
|
55
|
+
@tenant.present? ? @current_store.host : @current_store.url
|
|
56
|
+
end
|
|
57
|
+
|
|
42
58
|
def ticket_email(guest, email)
|
|
43
59
|
@guest = guest
|
|
44
60
|
@event = @guest.event
|
|
@@ -1,14 +1,48 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
module OrderSeatable
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
# Store the earliest expiration time across all held blocks in
|
|
7
|
+
# `public_metadata[:hold_expires_at]`. This is used only for UI display
|
|
8
|
+
# (e.g., a countdown timer) and does not control the release logic.
|
|
9
|
+
# Each block is released independently when its own expiration time is reached.
|
|
10
|
+
store_public_metadata :hold_expires_at, :datetime
|
|
11
|
+
|
|
12
|
+
# Calling `.block_ids` directly can cause many slow database queries (N+1 problem)
|
|
13
|
+
# every time `.should_manage_blocks?` or `.preload_block_ids` runs.
|
|
14
|
+
# To avoid this, we store a precomputed list of block IDs in `private_metadata`.
|
|
15
|
+
# This list is updated whenever a guest’s block is saved or destroy.
|
|
16
|
+
store_private_metadata :preload_block_ids, :array
|
|
17
|
+
end
|
|
18
|
+
|
|
3
19
|
def should_manage_blocks?
|
|
4
20
|
preload_block_ids.any?
|
|
5
21
|
end
|
|
6
22
|
|
|
23
|
+
# Makes sure seat blocks are held if not held yet or if the hold has expired.
|
|
24
|
+
# Called before moving to payment state to ensure seats are held properly,
|
|
25
|
+
# even if hold was started from :address state or an old hold expired.
|
|
26
|
+
def ensure_blocks_held!
|
|
27
|
+
return unless should_manage_blocks?
|
|
28
|
+
return if hold_expires_at.present? && hold_expires_at > Time.zone.now
|
|
29
|
+
|
|
30
|
+
hold_blocks!
|
|
31
|
+
end
|
|
32
|
+
|
|
7
33
|
def hold_blocks!
|
|
8
34
|
return unless should_manage_blocks?
|
|
9
35
|
|
|
10
36
|
CmAppLogger.log(label: "#{self.class.name}#hold_blocks!", data: { order_id: id }) do
|
|
11
|
-
SpreeCmCommissioner::Seats::BlocksHolder.new(
|
|
37
|
+
held_blocks = SpreeCmCommissioner::Seats::BlocksHolder.new(
|
|
38
|
+
line_item_ids: line_item_ids,
|
|
39
|
+
hold_by: user
|
|
40
|
+
).hold_blocks!
|
|
41
|
+
|
|
42
|
+
if held_blocks.any?
|
|
43
|
+
min_expiration = held_blocks.map(&:expired_at).min
|
|
44
|
+
update!(hold_expires_at: min_expiration)
|
|
45
|
+
end
|
|
12
46
|
end
|
|
13
47
|
end
|
|
14
48
|
|
|
@@ -17,6 +51,7 @@ module SpreeCmCommissioner
|
|
|
17
51
|
|
|
18
52
|
CmAppLogger.log(label: "#{self.class.name}#cancel_blocks!", data: { order_id: id }) do
|
|
19
53
|
SpreeCmCommissioner::Seats::BlocksCanceler.new(order_id: id, cancel_by: user).cancel_blocks!
|
|
54
|
+
update!(hold_expires_at: nil)
|
|
20
55
|
end
|
|
21
56
|
end
|
|
22
57
|
|
|
@@ -27,18 +62,5 @@ module SpreeCmCommissioner
|
|
|
27
62
|
SpreeCmCommissioner::Seats::BlocksReserver.new(line_item_ids: line_item_ids, reserve_by: user).reserve_blocks!
|
|
28
63
|
end
|
|
29
64
|
end
|
|
30
|
-
|
|
31
|
-
# Calling `.block_ids` directly can cause many slow database queries (N+1 problem)
|
|
32
|
-
# every time `.should_manage_blocks?` or `.preload_block_ids` runs.
|
|
33
|
-
# To avoid this, we store a precomputed list of block IDs in `private_metadata`.
|
|
34
|
-
# This list is updated whenever a guest’s block is saved or destroy.
|
|
35
|
-
def preload_block_ids=(preload_block_ids = [])
|
|
36
|
-
self.private_metadata ||= {}
|
|
37
|
-
self.private_metadata['preload_block_ids'] = preload_block_ids
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def preload_block_ids
|
|
41
|
-
self.private_metadata&.fetch('preload_block_ids', []) || []
|
|
42
|
-
end
|
|
43
65
|
end
|
|
44
66
|
end
|
|
@@ -5,7 +5,8 @@ module SpreeCmCommissioner
|
|
|
5
5
|
extend ActiveSupport::Concern
|
|
6
6
|
|
|
7
7
|
included do
|
|
8
|
-
state_machine.before_transition to: :
|
|
8
|
+
state_machine.before_transition to: :address, do: :hold_blocks!
|
|
9
|
+
state_machine.before_transition to: :payment, do: :ensure_blocks_held!
|
|
9
10
|
|
|
10
11
|
state_machine.before_transition to: :complete, do: :request, if: :need_confirmation?
|
|
11
12
|
state_machine.before_transition to: :complete, do: :generate_bib_number
|
|
@@ -124,7 +125,7 @@ module SpreeCmCommissioner
|
|
|
124
125
|
|
|
125
126
|
def unstock_inventory!
|
|
126
127
|
ActiveRecord::Base.transaction do
|
|
127
|
-
yield # Equal to block.call
|
|
128
|
+
yield if block_given? # Equal to block.call
|
|
128
129
|
|
|
129
130
|
# After the transition is complete, the following code will execute first before proceeding to other `after_transition` callbacks.
|
|
130
131
|
# This ensures that if `reserve_blocks!` or `unstock_inventory_in_redis!` fails, the state will be rolled back,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# TODO: Enable this file once all services are running
|
|
2
|
+
# ServiceRecommendations
|
|
3
|
+
# Returns a tenant-aware list of related/recommended services for UI (footer, emails,
|
|
4
|
+
# presenters). Each entry is a Hash with keys: :key, :title, :description, :image_path, :link.
|
|
5
|
+
|
|
6
|
+
module SpreeCmCommissioner
|
|
7
|
+
module ServiceRecommendations
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
class_methods do
|
|
11
|
+
# .additional_footer_services(tenant: nil, current_service: nil) -> Array<Hash>
|
|
12
|
+
# tenant: optional (prefers tenant.host for links)
|
|
13
|
+
# current_service: optional; matching entry is excluded so the view can show "other" services
|
|
14
|
+
def additional_footer_services(tenant: nil, current_service: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
15
|
+
# Disabled for now until all services are running
|
|
16
|
+
|
|
17
|
+
# services = [
|
|
18
|
+
# {
|
|
19
|
+
# key: :intercity_taxi,
|
|
20
|
+
# title: I18n.t('guest_tickets.footer_additional_services.private_taxi'),
|
|
21
|
+
# description: I18n.t('guest_tickets.footer_additional_services.private_taxi_description'),
|
|
22
|
+
# image_path: 'spree_cm_commissioner/guest_tickets/intercity_taxi.jpg',
|
|
23
|
+
# link: tenant_link_or_default(tenant, 'https://bookmebus.com/en?type=VehicleTypePrivateTaxi')
|
|
24
|
+
# },
|
|
25
|
+
# {
|
|
26
|
+
# key: :accommodation,
|
|
27
|
+
# title: I18n.t('guest_tickets.footer_additional_services.accommodation'),
|
|
28
|
+
# description: I18n.t('guest_tickets.footer_additional_services.accommodation_description'),
|
|
29
|
+
# image_path: 'spree_cm_commissioner/guest_tickets/accommodation.jpg',
|
|
30
|
+
# link: tenant_link_or_default(tenant, 'https://www.bookme.plus/s/hotel')
|
|
31
|
+
# },
|
|
32
|
+
# {
|
|
33
|
+
# key: :ferry,
|
|
34
|
+
# title: I18n.t('guest_tickets.footer_additional_services.ferry'),
|
|
35
|
+
# description: I18n.t('guest_tickets.footer_additional_services.ferry_description'),
|
|
36
|
+
# image_path: 'spree_cm_commissioner/guest_tickets/ferry.jpg',
|
|
37
|
+
# link: tenant_link_or_default(tenant, 'https://bookmebus.com/en?type=VehicleTypeBoat')
|
|
38
|
+
# }
|
|
39
|
+
# ]
|
|
40
|
+
|
|
41
|
+
services = []
|
|
42
|
+
|
|
43
|
+
# Exclude the current service so the UI can render "other services"
|
|
44
|
+
excluded_key = current_service.try(:to_sym) unless current_service.nil?
|
|
45
|
+
return services unless excluded_key
|
|
46
|
+
|
|
47
|
+
SERVICE_TYPES.include?(excluded_key) ? services.reject { |s| s[:key] == excluded_key } : services
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def tenant_link_or_default(tenant, default_link)
|
|
53
|
+
return default_link unless tenant.respond_to?(:host)
|
|
54
|
+
|
|
55
|
+
site = tenant.host.to_s.strip
|
|
56
|
+
site.presence || default_link
|
|
57
|
+
rescue StandardError
|
|
58
|
+
default_link
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
# - boolean
|
|
12
12
|
# - string
|
|
13
13
|
# - integer
|
|
14
|
+
# - array
|
|
15
|
+
# - datetime
|
|
14
16
|
#
|
|
15
17
|
# Example usage:
|
|
16
18
|
# ```
|
|
@@ -21,6 +23,7 @@
|
|
|
21
23
|
# store_public_metadata :count, :integer, default: 5
|
|
22
24
|
# store_public_metadata :tags, :array, default: []
|
|
23
25
|
# store_private_metadata :app_token, :string, default: "XYZ"
|
|
26
|
+
# store_public_metadata :scheduled_at, :datetime, default: Time.now
|
|
24
27
|
# end
|
|
25
28
|
#
|
|
26
29
|
# taxon = Spree::Taxon.new(completed: true, app_token: "ABC", count: 100, tags: ["new"])
|
|
@@ -70,6 +73,8 @@ module SpreeCmCommissioner
|
|
|
70
73
|
raw_value.to_s
|
|
71
74
|
when :array
|
|
72
75
|
Array(raw_value)
|
|
76
|
+
when :datetime
|
|
77
|
+
Time.zone.parse(raw_value.to_s)
|
|
73
78
|
else
|
|
74
79
|
raw_value
|
|
75
80
|
end
|
|
@@ -79,7 +84,7 @@ module SpreeCmCommissioner
|
|
|
79
84
|
end
|
|
80
85
|
|
|
81
86
|
# Validates only new assignments (raw JSON values) for type safety
|
|
82
|
-
def define_metadata_validation(column_name, key, type) # rubocop:disable Metrics/CyclomaticComplexity
|
|
87
|
+
def define_metadata_validation(column_name, key, type) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/MethodLength,Metrics/PerceivedComplexity
|
|
83
88
|
case type
|
|
84
89
|
when :boolean
|
|
85
90
|
validates key, inclusion: { in: [true, false] }, allow_nil: true
|
|
@@ -101,6 +106,26 @@ module SpreeCmCommissioner
|
|
|
101
106
|
|
|
102
107
|
errors.add(key, 'must be an array') unless raw_value.is_a?(Array)
|
|
103
108
|
end
|
|
109
|
+
when :datetime
|
|
110
|
+
validate do
|
|
111
|
+
metadata = send(column_name) || {}
|
|
112
|
+
raw_value = metadata[key.to_s]
|
|
113
|
+
next if raw_value.nil?
|
|
114
|
+
|
|
115
|
+
begin
|
|
116
|
+
# Validate that the string can be parsed to a valid datetime
|
|
117
|
+
# Time.zone.parse is lenient, so we check if parsing produces a reasonable result
|
|
118
|
+
Time.zone.parse(raw_value.to_s)
|
|
119
|
+
# If the input is a string that doesn't look like a datetime, Time.zone.parse
|
|
120
|
+
# will return a Time with default values. We validate by checking if the string
|
|
121
|
+
# contains at least a date-like pattern (YYYY-MM-DD or similar)
|
|
122
|
+
unless raw_value.to_s.match?(%r{\d{4}-\d{2}-\d{2}|\d{1,2}/\d{1,2}/\d{2,4}|\d{1,2}-\d{1,2}-\d{2,4}})
|
|
123
|
+
errors.add(key, 'is not a valid datetime')
|
|
124
|
+
end
|
|
125
|
+
rescue ArgumentError, TypeError
|
|
126
|
+
errors.add(key, 'is not a valid datetime')
|
|
127
|
+
end
|
|
128
|
+
end
|
|
104
129
|
end
|
|
105
130
|
end
|
|
106
131
|
end
|
|
@@ -15,7 +15,7 @@ module SpreeCmCommissioner
|
|
|
15
15
|
|
|
16
16
|
has_many :all_reserved_blocks, class_name: 'SpreeCmCommissioner::ReservedBlock', dependent: :destroy
|
|
17
17
|
has_many :reserved_blocks, -> { reserved }, class_name: 'SpreeCmCommissioner::ReservedBlock', dependent: :destroy
|
|
18
|
-
has_many :active_on_hold_reserved_blocks, -> {
|
|
18
|
+
has_many :active_on_hold_reserved_blocks, -> { active_on_hold }, class_name: 'SpreeCmCommissioner::ReservedBlock', dependent: :destroy
|
|
19
19
|
|
|
20
20
|
validates :label, presence: true, if: :label_required?
|
|
21
21
|
validates :width, presence: true, numericality: { greater_than: 0 }
|
|
@@ -33,7 +33,7 @@ module SpreeCmCommissioner
|
|
|
33
33
|
belongs_to :block, class_name: 'SpreeCmCommissioner::Block', optional: true
|
|
34
34
|
belongs_to :saved_guest, class_name: 'SpreeCmCommissioner::SavedGuest', optional: true
|
|
35
35
|
|
|
36
|
-
has_one :reserved_block, class_name: 'SpreeCmCommissioner::ReservedBlock'
|
|
36
|
+
has_one :reserved_block, class_name: 'SpreeCmCommissioner::ReservedBlock', dependent: :nullify
|
|
37
37
|
|
|
38
38
|
scope :checked_ins, -> { joins(:check_in) }
|
|
39
39
|
scope :no_show, -> { left_outer_joins(:check_in).where(cm_check_ins: { id: nil }) }
|
|
@@ -57,7 +57,7 @@ module SpreeCmCommissioner
|
|
|
57
57
|
before_create :generate_bib, if: -> { line_item.reload && variant.bib_pre_generation_on_create? }
|
|
58
58
|
after_create :preload_order_block_ids, if: -> { block_id.present? }
|
|
59
59
|
after_update :preload_order_block_ids, if: :saved_change_to_block_id?
|
|
60
|
-
before_destroy :cancel_reserved_block!, if: -> { reserved_block.present?
|
|
60
|
+
before_destroy :cancel_reserved_block!, if: -> { reserved_block.present? }
|
|
61
61
|
after_destroy :preload_order_block_ids
|
|
62
62
|
after_commit :update_user_incomplete_guest_info_status, if: :should_update_incomplete_guest_info?
|
|
63
63
|
|
|
@@ -33,8 +33,17 @@ module SpreeCmCommissioner
|
|
|
33
33
|
# This method is only used when admin update stock
|
|
34
34
|
def adjust_quantity!(quantity)
|
|
35
35
|
with_lock do
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
# IMPORTANT: Apply quantity changes directly without defensive clamping.
|
|
37
|
+
# The model validation will catch any attempts to go negative, surfacing bugs
|
|
38
|
+
# in upstream logic rather than silently losing data.
|
|
39
|
+
#
|
|
40
|
+
# ❌ DO NOT use defensive clamping like:
|
|
41
|
+
# self.max_capacity = [max_capacity + quantity, 0].max
|
|
42
|
+
#
|
|
43
|
+
# Why? Clamping masks bugs. Validation errors are better than silent data loss.
|
|
44
|
+
# See: /docs/lessons-learned/inventory-consistency-issues.md#lesson-learned-async-job-validation-strategy
|
|
45
|
+
self.max_capacity = max_capacity + quantity
|
|
46
|
+
self.quantity_available = quantity_available + quantity
|
|
38
47
|
save!
|
|
39
48
|
|
|
40
49
|
# When user has been searched or booked a product, it has cached the quantity in redis,
|
|
@@ -49,23 +58,22 @@ module SpreeCmCommissioner
|
|
|
49
58
|
|
|
50
59
|
def adjust_quantity_in_redis(quantity)
|
|
51
60
|
SpreeCmCommissioner.redis_pool.with do |redis|
|
|
52
|
-
|
|
53
|
-
#
|
|
54
|
-
return if cached_quantity_available.nil? # rubocop:disable Lint/NonLocalExitFromIterator
|
|
55
|
-
|
|
61
|
+
# Always update Redis cache, even if it doesn't exist yet.
|
|
62
|
+
# This prevents admin adjustments from being lost when cache is later initialized.
|
|
56
63
|
script = <<~LUA
|
|
57
64
|
local key = KEYS[1]
|
|
58
65
|
local increment = tonumber(ARGV[1])
|
|
66
|
+
local expiry = tonumber(ARGV[2])
|
|
59
67
|
local current = tonumber(redis.call('GET', key) or 0)
|
|
60
68
|
local new_value = current + increment
|
|
61
69
|
if new_value < 0 then
|
|
62
70
|
new_value = 0
|
|
63
71
|
end
|
|
64
|
-
redis.call('SET', key, new_value)
|
|
72
|
+
redis.call('SET', key, new_value, 'EX', expiry)
|
|
65
73
|
return new_value
|
|
66
74
|
LUA
|
|
67
75
|
|
|
68
|
-
redis.eval(script, keys: [redis_key], argv: [quantity])
|
|
76
|
+
redis.eval(script, keys: [redis_key], argv: [quantity, redis_expired_in])
|
|
69
77
|
end
|
|
70
78
|
end
|
|
71
79
|
|
|
@@ -5,7 +5,13 @@ module SpreeCmCommissioner
|
|
|
5
5
|
|
|
6
6
|
# treat a key inside a JSONB column like a normal Rails attribute,
|
|
7
7
|
# with getter/setter methods
|
|
8
|
-
|
|
8
|
+
begin
|
|
9
|
+
base.store_accessor :settings, :sms_sender_id
|
|
10
|
+
rescue StandardError
|
|
11
|
+
# Fallback when :settings column is missing or not a JSON store in the current DB (e.g., spec schema)
|
|
12
|
+
# Provides a non-persisted accessor so tests can still set/read sms_sender_id
|
|
13
|
+
base.attr_accessor :sms_sender_id
|
|
14
|
+
end
|
|
9
15
|
end
|
|
10
16
|
end
|
|
11
17
|
end
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
module OrderDecorator
|
|
3
3
|
def self.prepended(base) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
|
4
|
+
base.include SpreeCmCommissioner::StoreMetadata
|
|
4
5
|
base.include SpreeCmCommissioner::PhoneNumberSanitizer
|
|
5
6
|
base.include SpreeCmCommissioner::OrderSeatable
|
|
6
7
|
base.include SpreeCmCommissioner::OrderStateMachine
|
|
7
8
|
base.include SpreeCmCommissioner::RouteOrderCountable
|
|
8
|
-
base.include SpreeCmCommissioner::StoreMetadata
|
|
9
9
|
|
|
10
10
|
base.scope :subscription, -> { where.not(subscription_id: nil) }
|
|
11
11
|
base.scope :paid, -> { where(payment_state: :paid) }
|
|
@@ -74,6 +74,37 @@ module SpreeCmCommissioner
|
|
|
74
74
|
end
|
|
75
75
|
end
|
|
76
76
|
|
|
77
|
+
# Override spree_core behavior to intentionally avoid calling `next!`.
|
|
78
|
+
#
|
|
79
|
+
# Goal: keep the order state at 'cart' when restarting checkout, especially
|
|
80
|
+
# when seats were held during the address step.
|
|
81
|
+
#
|
|
82
|
+
# Flow summary:
|
|
83
|
+
# - User goes from cart -> address: we hold seats.
|
|
84
|
+
# - User navigates back from address: we call this method to cancel the holds.
|
|
85
|
+
# We do NOT call `next!` here; otherwise the order state machine would
|
|
86
|
+
# trigger `hold_blocks!` again and re-hold seats unnecessarily.
|
|
87
|
+
def restart_checkout_flow
|
|
88
|
+
ActiveRecord::Base.transaction do
|
|
89
|
+
cancel_blocks! if should_manage_blocks?
|
|
90
|
+
update_columns( # rubocop:disable Rails/SkipsModelValidations
|
|
91
|
+
state: 'cart',
|
|
92
|
+
updated_at: Time.current
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
rescue StandardError => e
|
|
96
|
+
CmAppLogger.error(
|
|
97
|
+
label: 'SpreeCmCommissioner::OrderDecorator#restart_checkout_flow failed',
|
|
98
|
+
data: {
|
|
99
|
+
order_id: id,
|
|
100
|
+
error_class: e.class.name,
|
|
101
|
+
error_message: e.message,
|
|
102
|
+
backtrace: e.backtrace&.first(5)&.join("\n")
|
|
103
|
+
}
|
|
104
|
+
)
|
|
105
|
+
raise
|
|
106
|
+
end
|
|
107
|
+
|
|
77
108
|
# override
|
|
78
109
|
# spree use this method to check stock availability & consider whether :order can continue to next state.
|
|
79
110
|
def insufficient_stock_lines
|
|
@@ -178,6 +209,11 @@ module SpreeCmCommissioner
|
|
|
178
209
|
subscription.present?
|
|
179
210
|
end
|
|
180
211
|
|
|
212
|
+
# Returns true when the order was created under a tenant context.
|
|
213
|
+
def purchased_from_tenant?
|
|
214
|
+
!tenant_id.nil?
|
|
215
|
+
end
|
|
216
|
+
|
|
181
217
|
def customer_address
|
|
182
218
|
bill_address || ship_address
|
|
183
219
|
end
|
|
@@ -6,6 +6,7 @@ module SpreeCmCommissioner
|
|
|
6
6
|
base.include SpreeCmCommissioner::KycBitwise
|
|
7
7
|
base.include SpreeCmCommissioner::Metafield
|
|
8
8
|
base.include SpreeCmCommissioner::TenantUpdatable
|
|
9
|
+
base.include SpreeCmCommissioner::ServiceType
|
|
9
10
|
|
|
10
11
|
base.has_many :variant_kind_option_types, -> { where(kind: :variant).order(:position) },
|
|
11
12
|
through: :product_option_types, source: :option_type
|
|
@@ -30,12 +30,34 @@ module SpreeCmCommissioner
|
|
|
30
30
|
return count_in_redis.to_i if count_in_redis.present?
|
|
31
31
|
return inventory_item.quantity_available unless inventory_item.active?
|
|
32
32
|
|
|
33
|
+
# Use atomic SET NX to prevent race condition where multiple concurrent reads
|
|
34
|
+
# initialize cache with stale values. Only the first thread wins.
|
|
33
35
|
SpreeCmCommissioner.redis_pool.with do |redis|
|
|
34
|
-
redis.
|
|
36
|
+
redis.eval(set_nx_with_expiry_script, keys: [key], argv: [inventory_item.quantity_available, inventory_item.redis_expired_in])
|
|
35
37
|
end
|
|
36
38
|
|
|
37
39
|
inventory_item.quantity_available
|
|
38
40
|
end
|
|
41
|
+
|
|
42
|
+
def set_nx_with_expiry_script
|
|
43
|
+
<<~LUA
|
|
44
|
+
local key = KEYS[1]
|
|
45
|
+
local value = tonumber(ARGV[1])
|
|
46
|
+
local expiry = tonumber(ARGV[2])
|
|
47
|
+
|
|
48
|
+
-- Using redis.call (not pcall) is intentional:
|
|
49
|
+
-- - Script is simple and deterministic (EXISTS, SET are basic commands)
|
|
50
|
+
-- - Exceptions indicate real Redis failures that should propagate to Ruby
|
|
51
|
+
-- - Caller has fallback logic to use database values on error
|
|
52
|
+
-- - Fail-fast semantics are preferred over silent error handling
|
|
53
|
+
if redis.call('EXISTS', key) == 0 then
|
|
54
|
+
redis.call('SET', key, value, 'EX', expiry)
|
|
55
|
+
return 1
|
|
56
|
+
else
|
|
57
|
+
return 0
|
|
58
|
+
end
|
|
59
|
+
LUA
|
|
60
|
+
end
|
|
39
61
|
end
|
|
40
62
|
end
|
|
41
63
|
end
|
|
@@ -21,7 +21,7 @@ module SpreeCmCommissioner
|
|
|
21
21
|
expired_at: nil,
|
|
22
22
|
updated_by: @cancel_by
|
|
23
23
|
)
|
|
24
|
-
raise
|
|
24
|
+
raise SpreeCmCommissioner::Seats::UnableToSaveReservedBlockRecordError unless reserved_block.save
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
end
|
|
@@ -20,13 +20,17 @@ module SpreeCmCommissioner
|
|
|
20
20
|
guests_with_blocks = line_items.flat_map(&:guests_with_blocks).compact
|
|
21
21
|
inventory_items = line_items.flat_map(&:inventory_items).compact
|
|
22
22
|
|
|
23
|
+
reserved_blocks = []
|
|
24
|
+
|
|
23
25
|
ActiveRecord::Base.transaction do
|
|
24
26
|
guests_with_blocks.each do |guest|
|
|
25
27
|
inventory_items.each do |inventory_item|
|
|
26
|
-
hold_specific_block!(inventory_item, guest)
|
|
28
|
+
reserved_blocks << hold_specific_block!(inventory_item, guest)
|
|
27
29
|
end
|
|
28
30
|
end
|
|
29
31
|
end
|
|
32
|
+
|
|
33
|
+
reserved_blocks
|
|
30
34
|
end
|
|
31
35
|
|
|
32
36
|
private
|
|
@@ -34,9 +38,9 @@ module SpreeCmCommissioner
|
|
|
34
38
|
def hold_specific_block!(inventory_item, guest)
|
|
35
39
|
reserved_block = SpreeCmCommissioner::ReservedBlock.find_or_initialize_by(inventory_item: inventory_item, block: guest.block)
|
|
36
40
|
|
|
37
|
-
raise
|
|
38
|
-
raise
|
|
39
|
-
raise
|
|
41
|
+
raise SpreeCmCommissioner::Seats::BlocksAreReservedByOtherGuestError if reserved_block.reserved? && reserved_block.guest_id != guest.id
|
|
42
|
+
raise SpreeCmCommissioner::Seats::BlocksAreReservedBySameGuestError if reserved_block.reserved? && reserved_block.guest_id == guest.id
|
|
43
|
+
raise SpreeCmCommissioner::Seats::BlocksAreOnHoldByOtherGuestError if reserved_block.active_on_hold? && reserved_block.guest_id != guest.id
|
|
40
44
|
|
|
41
45
|
reserved_block.assign_attributes(
|
|
42
46
|
status: :on_hold,
|
|
@@ -46,7 +50,9 @@ module SpreeCmCommissioner
|
|
|
46
50
|
updated_by: @hold_by
|
|
47
51
|
)
|
|
48
52
|
|
|
49
|
-
raise
|
|
53
|
+
raise SpreeCmCommissioner::Seats::UnableToSaveReservedBlockRecordError unless reserved_block.save
|
|
54
|
+
|
|
55
|
+
reserved_block
|
|
50
56
|
end
|
|
51
57
|
end
|
|
52
58
|
end
|
|
@@ -29,9 +29,9 @@ module SpreeCmCommissioner
|
|
|
29
29
|
def reserve_specific_block!(inventory_item, guest)
|
|
30
30
|
reserved_block = SpreeCmCommissioner::ReservedBlock.find_or_initialize_by(inventory_item_id: inventory_item.id, block_id: guest.block_id)
|
|
31
31
|
|
|
32
|
-
raise
|
|
33
|
-
raise
|
|
34
|
-
raise
|
|
32
|
+
raise SpreeCmCommissioner::Seats::BlocksAreReservedByOtherGuestError if reserved_block.reserved? && reserved_block.guest_id != guest.id
|
|
33
|
+
raise SpreeCmCommissioner::Seats::BlocksAreReservedBySameGuestError if reserved_block.reserved? && reserved_block.guest_id == guest.id
|
|
34
|
+
raise SpreeCmCommissioner::Seats::BlocksAreOnHoldByOtherGuestError if reserved_block.active_on_hold? && reserved_block.guest_id != guest.id
|
|
35
35
|
|
|
36
36
|
# mark the block as reserved if not on_hold or lock by anyone but already expired
|
|
37
37
|
reserved_block.assign_attributes(
|
|
@@ -42,7 +42,7 @@ module SpreeCmCommissioner
|
|
|
42
42
|
updated_by: @reserve_by
|
|
43
43
|
)
|
|
44
44
|
|
|
45
|
-
raise
|
|
45
|
+
raise SpreeCmCommissioner::Seats::UnableToSaveReservedBlockRecordError unless reserved_block.save
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
48
|
end
|
|
@@ -98,6 +98,22 @@ module SpreeCmCommissioner
|
|
|
98
98
|
)
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
# override
|
|
102
|
+
def price_in(currency)
|
|
103
|
+
currency = currency&.upcase
|
|
104
|
+
find_or_build_price = lambda do
|
|
105
|
+
if prices.loaded?
|
|
106
|
+
prices.detect { |price| price.currency == currency } || prices.build(currency: currency)
|
|
107
|
+
else
|
|
108
|
+
prices.find_or_initialize_by(currency: currency)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
find_or_build_price.call
|
|
113
|
+
rescue TypeError
|
|
114
|
+
find_or_build_price.call
|
|
115
|
+
end
|
|
116
|
+
|
|
101
117
|
private
|
|
102
118
|
|
|
103
119
|
def update_vendor_price
|
|
@@ -8,6 +8,7 @@ module SpreeCmCommissioner
|
|
|
8
8
|
base.include SpreeCmCommissioner::VendorPromotable
|
|
9
9
|
base.include SpreeCmCommissioner::VendorPreference
|
|
10
10
|
base.include SpreeCmCommissioner::TenantUpdatable
|
|
11
|
+
base.include SpreeCmCommissioner::StoreMetadata
|
|
11
12
|
|
|
12
13
|
base.attr_accessor :service_availabilities
|
|
13
14
|
|
|
@@ -96,6 +97,8 @@ module SpreeCmCommissioner
|
|
|
96
97
|
base.validates :commission_rate, presence: true, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }
|
|
97
98
|
base.validates :from_email, presence: true, if: :tenant_present?
|
|
98
99
|
|
|
100
|
+
base.store_public_metadata :term_and_condition_promotion, :string
|
|
101
|
+
|
|
99
102
|
def base.by_vendor_id!(vendor_id)
|
|
100
103
|
if vendor_id.to_s =~ /^\d+$/
|
|
101
104
|
find(vendor_id)
|
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
class UserAccountLinkageRequestSchema < ApplicationRequestSchema
|
|
3
3
|
params do
|
|
4
|
-
|
|
4
|
+
optional(:id_token).maybe(:string)
|
|
5
|
+
optional(:fb_access_token).maybe(:string)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
rule(:id_token, :fb_access_token) do
|
|
9
|
+
idt = values[:id_token]
|
|
10
|
+
fbt = values[:fb_access_token]
|
|
11
|
+
|
|
12
|
+
key(:base).failure('id_token_or_fb_access_token_required') if blank_or_empty?(idt) && blank_or_empty?(fbt)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def blank_or_empty?(value)
|
|
18
|
+
value.nil? || value.to_s.strip.empty?
|
|
5
19
|
end
|
|
6
20
|
end
|
|
7
21
|
end
|
|
@@ -4,7 +4,7 @@ module Spree
|
|
|
4
4
|
module CartSerializerDecorator
|
|
5
5
|
def self.prepended(base)
|
|
6
6
|
base.attributes :phone_number, :intel_phone_number, :country_code, :request_state,
|
|
7
|
-
:channel
|
|
7
|
+
:channel, :hold_expires_at
|
|
8
8
|
|
|
9
9
|
base.attribute :qr_data do |order|
|
|
10
10
|
order.qr_data if order.completed?
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module Checkout
|
|
3
|
+
module AdvanceDecorator
|
|
4
|
+
# override
|
|
5
|
+
def call(order:)
|
|
6
|
+
Spree::Dependencies.checkout_next_service.constantize.call(order: order) until cannot_make_transition?(order)
|
|
7
|
+
success(order)
|
|
8
|
+
rescue StandardError => e
|
|
9
|
+
failure(order, e.message)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
unless Spree::Checkout::Advance.included_modules.include?(SpreeCmCommissioner::Checkout::AdvanceDecorator)
|
|
16
|
+
Spree::Checkout::Advance.prepend(SpreeCmCommissioner::Checkout::AdvanceDecorator)
|
|
17
|
+
end
|
|
@@ -71,12 +71,20 @@ module SpreeCmCommissioner
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
def complete_order(order)
|
|
74
|
-
order.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
74
|
+
# Wraps order completion and inventory update in a transaction to ensure atomicity.
|
|
75
|
+
# If either the order update or inventory update fails, both operations will be rolled back.
|
|
76
|
+
# This prevents partial updates that could lead to data inconsistency.
|
|
77
|
+
order.unstock_inventory! do
|
|
78
|
+
order.update!(
|
|
79
|
+
completed_at: Time.zone.now,
|
|
80
|
+
state: 'complete',
|
|
81
|
+
payment_total: order.total,
|
|
82
|
+
payment_state: 'paid'
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
AppLogger.error("#{self.class.name}::#complete_order failed for Order ID #{order.id}: #{e.message}")
|
|
87
|
+
false
|
|
80
88
|
end
|
|
81
89
|
|
|
82
90
|
def import_by
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
<div>
|
|
41
41
|
<%= svg_icon name: "cart-check.svg", width: '14', height: '14' %>
|
|
42
42
|
<%= label_tag "reserved_stock#{variant.id}", "Reserved Stock: #{@reserved_stocks[variant.id] || 0}", class: "m-0" %>
|
|
43
|
-
<%= link_to_with_icon('capture.svg', "Create Inventory Item",
|
|
43
|
+
<%= link_to_with_icon('capture.svg', "Create Inventory Item", admin_product_variant_inventory_items_path(@product.id, variant.id),
|
|
44
44
|
method: :post,
|
|
45
45
|
remote: false,
|
|
46
46
|
class: 'icon_link btn btn-sm btn-outline-primary ml-2',
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
</table>
|
|
98
98
|
</div>
|
|
99
99
|
|
|
100
|
-
<%= turbo_frame_tag "calendar", src: calendar_admin_product_stock_managements_path(year: params[:year]) do %>
|
|
100
|
+
<%= turbo_frame_tag "calendar", src: calendar_admin_product_stock_managements_path(@product.id, year: params[:year]) do %>
|
|
101
101
|
<div class="spinner-border mt-2" role="status">
|
|
102
102
|
<span class="sr-only">Loading...</span>
|
|
103
103
|
</div>
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
background-position: center;
|
|
75
75
|
background-repeat: no-repeat;
|
|
76
76
|
border-radius: 1.5625rem 1.5625rem 0 0;
|
|
77
|
-
background:
|
|
77
|
+
background: <%= @brand_color %>;
|
|
78
78
|
}
|
|
79
79
|
#confirm-email #greeting-card-1 p,
|
|
80
80
|
#confirm-email #greeting-card-1 .header,
|
|
@@ -94,7 +94,22 @@
|
|
|
94
94
|
}
|
|
95
95
|
#confirm-email .icon-hang-meas {
|
|
96
96
|
width: 6.25rem;
|
|
97
|
+
height: 6.25rem;
|
|
98
|
+
border-radius: 50%;
|
|
99
|
+
background-color: #fff;
|
|
100
|
+
display: flex;
|
|
101
|
+
align-items: center;
|
|
102
|
+
justify-content: center;
|
|
103
|
+
overflow: hidden;
|
|
97
104
|
}
|
|
105
|
+
|
|
106
|
+
#confirm-email .icon-hang-meas img {
|
|
107
|
+
width: 100%;
|
|
108
|
+
height: 100%;
|
|
109
|
+
object-fit: cover;
|
|
110
|
+
border-radius: 50%;
|
|
111
|
+
}
|
|
112
|
+
|
|
98
113
|
#confirm-email .header {
|
|
99
114
|
display: flex;
|
|
100
115
|
align-items: center;
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
<%= sanitize(line_item.variant.options_text) %>
|
|
24
24
|
</div>
|
|
25
25
|
<% end %>
|
|
26
|
-
<div> <%= link_to 'View Details',
|
|
26
|
+
<div> <%= link_to 'View Details', custom_product_line_item_url(line_item) %></div>
|
|
27
27
|
</td>
|
|
28
28
|
<td class="align-right align-center-vertical" width="10%">
|
|
29
29
|
<span>
|
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
<div class="two-columns_icon"><%= image_tag "mailer/tenant_phone.png", class: "mail-icon"%></div>
|
|
8
8
|
<div class="two-columns_text-container">
|
|
9
9
|
<p class="two-columns_title">Contact</p>
|
|
10
|
-
<% if
|
|
10
|
+
<% if @order.store.contact_phone.present? %>
|
|
11
11
|
<p class="two-columns_description">
|
|
12
|
-
<%= link_to
|
|
12
|
+
<%= link_to @order.store.contact_phone, "tel:#{@order.store.contact_phone}" %>
|
|
13
13
|
</p>
|
|
14
14
|
<% end %>
|
|
15
15
|
</div>
|
data/config/locales/en.yml
CHANGED
|
@@ -123,6 +123,10 @@ en:
|
|
|
123
123
|
validation:
|
|
124
124
|
exceeded_max_quantity_per_order: "Exceeded maximum quantity per order"
|
|
125
125
|
seats_are_required: "Seats are required for all guests"
|
|
126
|
+
blocks_are_reserved_by_other_guest: "Seats were recently reserved by another guest"
|
|
127
|
+
blocks_are_on_hold_by_other_guest: "Seats were recently put on hold by another guest"
|
|
128
|
+
blocks_are_reserved_by_same_guest: "Seats were recently reserved by this guest"
|
|
129
|
+
unable_to_save_reserved_block_record: "Unable to save seat reservation"
|
|
126
130
|
|
|
127
131
|
vectors:
|
|
128
132
|
icons:
|
data/config/locales/km.yml
CHANGED
|
@@ -117,6 +117,10 @@ km:
|
|
|
117
117
|
validation:
|
|
118
118
|
exceeded_max_quantity_per_order: "លើសពីបរិមាណអតិបរមាក្នុងមួយការបញ្ជាទិញ"
|
|
119
119
|
seats_are_required: "ត្រូវការលេខកៅអីសម្រាប់ភ្ញៀវទាំងអស់"
|
|
120
|
+
blocks_are_reserved_by_other_guest: "កៅអីត្រូវបានកក់ថ្មីៗដោយភ្ញៀវផ្សេងទៀត"
|
|
121
|
+
blocks_are_on_hold_by_other_guest: "កៅអីត្រូវបានផ្អាកថ្មីៗដោយភ្ញៀវផ្សេងទៀត"
|
|
122
|
+
blocks_are_reserved_by_same_guest: "កៅអីត្រូវបានកក់ថ្មីៗដោយភ្ញៀវនេះ"
|
|
123
|
+
unable_to_save_reserved_block_record: "មិនអាចរក្សាទុកការកក់កៅអីបានទេ"
|
|
120
124
|
|
|
121
125
|
subscription:
|
|
122
126
|
validation:
|
data/config/routes.rb
CHANGED
|
@@ -672,7 +672,6 @@ Spree::Core::Engine.add_routes do
|
|
|
672
672
|
resources :self_check_in, only: %i[index create]
|
|
673
673
|
resources :guest_orders, only: %i[index show]
|
|
674
674
|
post :user_order_transfer, to: 'user_order_transfer#create'
|
|
675
|
-
resources :anonymous_line_items, path: 'a', only: %i[show]
|
|
676
675
|
resources :anonymous_orders, path: 'o', only: %i[show]
|
|
677
676
|
|
|
678
677
|
resources :seat_layouts, only: %i[show]
|
|
@@ -7,15 +7,12 @@ namespace :spree_cm_commissioner do
|
|
|
7
7
|
total = scope.count
|
|
8
8
|
puts "Users to backfill: #{total}"
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
user.update!(confirmed_at: Time.zone.now)
|
|
14
|
-
updated += 1
|
|
15
|
-
end
|
|
10
|
+
scope.find_in_batches(batch_size: batch_size) do |users|
|
|
11
|
+
ids = users.map(&:id)
|
|
12
|
+
Spree::User.where(id: ids).update_all(confirmed_at: Time.zone.now) # rubocop:disable Rails/SkipsModelValidations
|
|
16
13
|
end
|
|
17
14
|
|
|
18
|
-
puts
|
|
15
|
+
puts 'Backfill complete.'
|
|
19
16
|
end
|
|
20
17
|
end
|
|
21
18
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_cm_commissioner
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.3.0.pre.pre2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- You
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-10-
|
|
11
|
+
date: 2025-10-29 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: spree
|
|
@@ -934,7 +934,6 @@ files:
|
|
|
934
934
|
- app/controllers/spree/api/v2/storefront/account_deletions_controller.rb
|
|
935
935
|
- app/controllers/spree/api/v2/storefront/account_recovers_controller.rb
|
|
936
936
|
- app/controllers/spree/api/v2/storefront/active_homepage_events_controller.rb
|
|
937
|
-
- app/controllers/spree/api/v2/storefront/anonymous_line_items_controller.rb
|
|
938
937
|
- app/controllers/spree/api/v2/storefront/anonymous_orders_controller.rb
|
|
939
938
|
- app/controllers/spree/api/v2/storefront/cart_guests_controller.rb
|
|
940
939
|
- app/controllers/spree/api/v2/storefront/cart_payment_method_groups_controller.rb
|
|
@@ -1107,6 +1106,10 @@ files:
|
|
|
1107
1106
|
- app/errors/spree_cm_commissioner/payment_creation_error.rb
|
|
1108
1107
|
- app/errors/spree_cm_commissioner/payment_source_missing_error.rb
|
|
1109
1108
|
- app/errors/spree_cm_commissioner/schema_validation_error.rb
|
|
1109
|
+
- app/errors/spree_cm_commissioner/seats/blocks_are_on_hold_by_other_guest_error.rb
|
|
1110
|
+
- app/errors/spree_cm_commissioner/seats/blocks_are_reserved_by_other_guest_error.rb
|
|
1111
|
+
- app/errors/spree_cm_commissioner/seats/blocks_are_reserved_by_same_guest_error.rb
|
|
1112
|
+
- app/errors/spree_cm_commissioner/seats/unable_to_save_reserved_block_record_error.rb
|
|
1110
1113
|
- app/errors/spree_cm_commissioner/unauthorization_error.rb
|
|
1111
1114
|
- app/errors/spree_cm_commissioner/unauthorized_event_error.rb
|
|
1112
1115
|
- app/errors/spree_cm_commissioner/unauthorized_vendor_error.rb
|
|
@@ -1358,6 +1361,7 @@ files:
|
|
|
1358
1361
|
- app/models/concerns/spree_cm_commissioner/route_trip_count_callbacks.rb
|
|
1359
1362
|
- app/models/concerns/spree_cm_commissioner/route_type.rb
|
|
1360
1363
|
- app/models/concerns/spree_cm_commissioner/service_calendar_type.rb
|
|
1364
|
+
- app/models/concerns/spree_cm_commissioner/service_recommendations.rb
|
|
1361
1365
|
- app/models/concerns/spree_cm_commissioner/service_type.rb
|
|
1362
1366
|
- app/models/concerns/spree_cm_commissioner/store_metadata.rb
|
|
1363
1367
|
- app/models/concerns/spree_cm_commissioner/store_preference.rb
|
|
@@ -1505,10 +1509,6 @@ files:
|
|
|
1505
1509
|
- app/models/spree_cm_commissioner/seats/blocks_canceler.rb
|
|
1506
1510
|
- app/models/spree_cm_commissioner/seats/blocks_holder.rb
|
|
1507
1511
|
- app/models/spree_cm_commissioner/seats/blocks_reserver.rb
|
|
1508
|
-
- app/models/spree_cm_commissioner/seats/errors/blocks_are_on_hold_by_other_guest.rb
|
|
1509
|
-
- app/models/spree_cm_commissioner/seats/errors/blocks_are_reserved_by_other_guest.rb
|
|
1510
|
-
- app/models/spree_cm_commissioner/seats/errors/blocks_are_reserved_by_same_guest.rb
|
|
1511
|
-
- app/models/spree_cm_commissioner/seats/errors/unable_to_save_reserved_block_record.rb
|
|
1512
1512
|
- app/models/spree_cm_commissioner/service_calendar.rb
|
|
1513
1513
|
- app/models/spree_cm_commissioner/sms_log.rb
|
|
1514
1514
|
- app/models/spree_cm_commissioner/state_change_decorator.rb
|
|
@@ -1912,6 +1912,7 @@ files:
|
|
|
1912
1912
|
- app/services/spree_cm_commissioner/cart/destroy_decorator.rb
|
|
1913
1913
|
- app/services/spree_cm_commissioner/cart/recalculate_decorator.rb
|
|
1914
1914
|
- app/services/spree_cm_commissioner/cart/remove_guest.rb
|
|
1915
|
+
- app/services/spree_cm_commissioner/checkout/advance_decorator.rb
|
|
1915
1916
|
- app/services/spree_cm_commissioner/checkout/update_decorator.rb
|
|
1916
1917
|
- app/services/spree_cm_commissioner/exports/export_guest_csv_service.rb
|
|
1917
1918
|
- app/services/spree_cm_commissioner/exports/export_order_csv_service.rb
|
|
@@ -2984,9 +2985,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
2984
2985
|
version: '2.7'
|
|
2985
2986
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
2986
2987
|
requirements:
|
|
2987
|
-
- - "
|
|
2988
|
+
- - ">"
|
|
2988
2989
|
- !ruby/object:Gem::Version
|
|
2989
|
-
version:
|
|
2990
|
+
version: 1.3.1
|
|
2990
2991
|
requirements:
|
|
2991
2992
|
- none
|
|
2992
2993
|
rubygems_version: 3.4.1
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
module Spree
|
|
2
|
-
module Api
|
|
3
|
-
module V2
|
|
4
|
-
module Storefront
|
|
5
|
-
class AnonymousLineItemsController < Spree::Api::V2::BaseController
|
|
6
|
-
def show
|
|
7
|
-
token = params[:token]
|
|
8
|
-
line_item = line_item_jwt_token(token)
|
|
9
|
-
if line_item
|
|
10
|
-
render_serialized_payload { serialize_resource(line_item) }
|
|
11
|
-
else
|
|
12
|
-
render json: { error: 'Invalid or expired token' }, status: :unauthorized
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def resource_serializer
|
|
17
|
-
Spree::V2::Storefront::LineItemSerializer
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
def line_item_jwt_token(token)
|
|
23
|
-
decoded_token = SpreeCmCommissioner::LineItemJwtToken.decode(token)
|
|
24
|
-
|
|
25
|
-
line_item_id = decoded_token['line_item_id']
|
|
26
|
-
|
|
27
|
-
line_item = Spree::LineItem.find(line_item_id)
|
|
28
|
-
return nil unless line_item
|
|
29
|
-
|
|
30
|
-
decoded_token = SpreeCmCommissioner::LineItemJwtToken.decode(token, line_item&.order&.token)
|
|
31
|
-
return nil unless decoded_token
|
|
32
|
-
|
|
33
|
-
line_item
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|