spree_cm_commissioner 2.2.1 → 2.3.0.pre.pre1

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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/controllers/spree/api/v2/tenant/user_account_linkages_controller.rb +5 -1
  4. data/app/errors/spree_cm_commissioner/seats/blocks_are_on_hold_by_other_guest_error.rb +8 -0
  5. data/app/errors/spree_cm_commissioner/seats/blocks_are_reserved_by_other_guest_error.rb +8 -0
  6. data/app/errors/spree_cm_commissioner/seats/blocks_are_reserved_by_same_guest_error.rb +8 -0
  7. data/app/errors/spree_cm_commissioner/seats/unable_to_save_reserved_block_record_error.rb +8 -0
  8. data/app/helpers/spree/base_helper_decorator.rb +1 -1
  9. data/app/interactors/spree_cm_commissioner/account_linkage.rb +28 -5
  10. data/app/interactors/spree_cm_commissioner/create_ticket.rb +6 -4
  11. data/app/interactors/spree_cm_commissioner/inventory_item_syncer.rb +10 -0
  12. data/app/interactors/spree_cm_commissioner/order_importer/multi_guest.rb +9 -1
  13. data/app/interactors/spree_cm_commissioner/order_importer/single_guest.rb +9 -1
  14. data/app/interactors/spree_cm_commissioner/pin_code_creator.rb +9 -1
  15. data/app/mailers/spree/order_mailer_decorator.rb +26 -10
  16. data/app/models/concerns/spree_cm_commissioner/order_seatable.rb +30 -1
  17. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +3 -2
  18. data/app/models/concerns/spree_cm_commissioner/service_recommendations.rb +62 -0
  19. data/app/models/concerns/spree_cm_commissioner/store_metadata.rb +26 -1
  20. data/app/models/spree_cm_commissioner/block.rb +1 -1
  21. data/app/models/spree_cm_commissioner/guest.rb +2 -2
  22. data/app/models/spree_cm_commissioner/inventory_item.rb +16 -8
  23. data/app/models/spree_cm_commissioner/oauth_application_decorator.rb +7 -1
  24. data/app/models/spree_cm_commissioner/order_decorator.rb +37 -1
  25. data/app/models/spree_cm_commissioner/product_decorator.rb +1 -0
  26. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +23 -1
  27. data/app/models/spree_cm_commissioner/seats/blocks_canceler.rb +1 -1
  28. data/app/models/spree_cm_commissioner/seats/blocks_holder.rb +11 -5
  29. data/app/models/spree_cm_commissioner/seats/blocks_reserver.rb +4 -4
  30. data/app/models/spree_cm_commissioner/variant_decorator.rb +16 -0
  31. data/app/models/spree_cm_commissioner/vendor_decorator.rb +3 -0
  32. data/app/request_schemas/spree_cm_commissioner/user_account_linkage_request_schema.rb +15 -1
  33. data/app/serializers/spree/v2/storefront/cart_serializer_decorator.rb +1 -1
  34. data/app/services/spree_cm_commissioner/checkout/advance_decorator.rb +17 -0
  35. data/app/services/spree_cm_commissioner/imports/create_order_service.rb +14 -6
  36. data/app/views/spree/admin/stock_managements/index.html.erb +2 -2
  37. data/app/views/spree_cm_commissioner/order_mailer/_mailer_stylesheets.html.erb +16 -1
  38. data/app/views/spree_cm_commissioner/order_mailer/purchased_items/_items.html.erb +1 -1
  39. data/app/views/spree_cm_commissioner/order_mailer/tenant/_greeting.html.erb +1 -1
  40. data/app/views/spree_cm_commissioner/order_mailer/tenant/_support_contact.html.erb +2 -2
  41. data/config/locales/en.yml +4 -0
  42. data/config/locales/km.yml +4 -0
  43. data/config/routes.rb +0 -1
  44. data/lib/spree_cm_commissioner/version.rb +1 -1
  45. data/lib/tasks/backfill_confirmed_at.rake +4 -7
  46. metadata +10 -9
  47. data/app/controllers/spree/api/v2/storefront/anonymous_line_items_controller.rb +0 -39
  48. data/app/models/spree_cm_commissioner/seats/errors/blocks_are_on_hold_by_other_guest.rb +0 -4
  49. data/app/models/spree_cm_commissioner/seats/errors/blocks_are_reserved_by_other_guest.rb +0 -4
  50. data/app/models/spree_cm_commissioner/seats/errors/blocks_are_reserved_by_same_guest.rb +0 -4
  51. 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: feeb4617fbf2f6a3541a5240390c2926f3bfa9e8710ea868503b23438c344ead
4
- data.tar.gz: c33d36138db885a6db7874057c4f48587d8f4a0b2df444bb4b4850d62b0cf7b6
3
+ metadata.gz: 2dabafba4cdf4fd469f2a2788f523d0d21ea86be92d69acfa5d7a063d23ba940
4
+ data.tar.gz: 97eef923bdf9bb7ccceba698afb569f21eec06c11f7ea2f15e9b5b2f2d336ad3
5
5
  SHA512:
6
- metadata.gz: 36a58209ead85a28a4381217d6781fe86b804a8f5fd4762dda83abf7a11b85d19b9230a37737447400a72578708b71097e647a7e545636eb7b27fe7cea3cdabe
7
- data.tar.gz: 5447edba53aa8b0899f9e7fc2a37a56ab705d5c1a334520c3fd2367c6edf6b1a133bef8ce08eb60047565041c59ab9ab5c8b1e24f76833d6cbbe34e020e27994
6
+ metadata.gz: de0e2d62d1d799d8f929047517de05254e033b51e60cd177f589fe6f0a040efe70dd02fea4ade2309cf47b7594cac145eb3712f1a9aa9223c70f73e14911478b
7
+ data.tar.gz: 91ff564a418a1aa7aa228455dae458bc856ced5e758558375080d9d5280d64b81603426174335749c8cc56152466e38b75c402b6feecd1d77cb897c2f416a2e8
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.2.1)
37
+ spree_cm_commissioner (2.3.0.pre.pre1)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -12,7 +12,11 @@ module Spree
12
12
  end
13
13
 
14
14
  def create
15
- context = SpreeCmCommissioner::AccountLinkage.call(user: spree_current_user, id_token: params[:id_token])
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
@@ -0,0 +1,8 @@
1
+ module SpreeCmCommissioner
2
+ class Seats::BlocksAreOnHoldByOtherGuestError < StandardError
3
+ # override
4
+ def message
5
+ I18n.t('line_item.validation.blocks_are_on_hold_by_other_guest')
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module SpreeCmCommissioner
2
+ class Seats::BlocksAreReservedByOtherGuestError < StandardError
3
+ # override
4
+ def message
5
+ I18n.t('line_item.validation.blocks_are_reserved_by_other_guest')
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module SpreeCmCommissioner
2
+ class Seats::BlocksAreReservedBySameGuestError < StandardError
3
+ # override
4
+ def message
5
+ I18n.t('line_item.validation.blocks_are_reserved_by_same_guest')
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ module SpreeCmCommissioner
2
+ class Seats::UnableToSaveReservedBlockRecordError < StandardError
3
+ # override
4
+ def message
5
+ I18n.t('line_item.validation.unable_to_save_reserved_block_record')
6
+ end
7
+ end
8
+ end
@@ -38,7 +38,7 @@ module Spree
38
38
 
39
39
  return if line_item.number.blank? && jwt_token.blank?
40
40
 
41
- "#{current_store.formatted_url + localize}/a/#{line_item.number}?ak=#{jwt_token}"
41
+ "#{current_store.formatted_url + localize}/a/#{line_item.qr_data}"
42
42
  end
43
43
  end
44
44
  end
@@ -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
- firebase_id_token_context = FirebaseIdTokenProvider.call(id_token: context.id_token)
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
- private
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
- identity_type = provider[:identity_type]
25
-
26
- identity_type = UserIdentityProvider.identity_types[identity_type]
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
- @stock_item = Spree::StockItem.new(
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
- count_on_hand: context.params[:count_on_hand]
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
- context.fail!(message: @stock_item.errors.full_messages.join(', '))
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
- context.fail!(message: context.order.errors.full_messages.to_sentence) unless context.order.save
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
- context.fail!(message: order.errors.full_messages.to_sentence) unless context.order.save
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, contact_type: context.contact_type, type: context.type, application: context.application }
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
- @tenant = @order.tenant
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 = resend ? "[#{Spree.t(:resend).upcase}] " : ''
24
- subject += "#{@current_store&.name} Booking Confirmation ##{@order.number}"
17
+ subject = build_subject(resend)
25
18
 
26
- mail(to: @order.email, from: from_email_address, subject: subject, store_url: @current_store.url) do |format|
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,42 @@
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
+ end
12
+
3
13
  def should_manage_blocks?
4
14
  preload_block_ids.any?
5
15
  end
6
16
 
17
+ # Makes sure seat blocks are held if not held yet or if the hold has expired.
18
+ # Called before moving to payment state to ensure seats are held properly,
19
+ # even if hold was started from :address state or an old hold expired.
20
+ def ensure_blocks_held!
21
+ return unless should_manage_blocks?
22
+ return if hold_expires_at.present? && hold_expires_at > Time.zone.now
23
+
24
+ hold_blocks!
25
+ end
26
+
7
27
  def hold_blocks!
8
28
  return unless should_manage_blocks?
9
29
 
10
30
  CmAppLogger.log(label: "#{self.class.name}#hold_blocks!", data: { order_id: id }) do
11
- SpreeCmCommissioner::Seats::BlocksHolder.new(line_item_ids: line_item_ids, hold_by: user).hold_blocks!
31
+ held_blocks = SpreeCmCommissioner::Seats::BlocksHolder.new(
32
+ line_item_ids: line_item_ids,
33
+ hold_by: user
34
+ ).hold_blocks!
35
+
36
+ if held_blocks.any?
37
+ min_expiration = held_blocks.map(&:expired_at).min
38
+ update!(hold_expires_at: min_expiration)
39
+ end
12
40
  end
13
41
  end
14
42
 
@@ -17,6 +45,7 @@ module SpreeCmCommissioner
17
45
 
18
46
  CmAppLogger.log(label: "#{self.class.name}#cancel_blocks!", data: { order_id: id }) do
19
47
  SpreeCmCommissioner::Seats::BlocksCanceler.new(order_id: id, cancel_by: user).cancel_blocks!
48
+ update!(hold_expires_at: nil)
20
49
  end
21
50
  end
22
51
 
@@ -5,7 +5,8 @@ module SpreeCmCommissioner
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
- state_machine.before_transition to: :payment, do: :hold_blocks!
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, -> { reserved_or_on_hold }, class_name: 'SpreeCmCommissioner::ReservedBlock', dependent: :destroy
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? && reserved_block.active_on_hold? }
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
- self.max_capacity = [max_capacity + quantity, 0].max
37
- self.quantity_available = [quantity_available + quantity, 0].max
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
- cached_quantity_available = redis.get(redis_key)
53
- # ignore if redis doesn't exist
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
- base.store_accessor :settings, :sms_sender_id
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.set(key, inventory_item.quantity_available, ex: inventory_item.redis_expired_in)
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 Errors::UnableToSaveReservedBlockRecord unless reserved_block.save
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 Errors::BlocksAreReservedByOtherGuest if reserved_block.reserved? && reserved_block.guest_id != guest.id
38
- raise Errors::BlocksAreReservedBySameGuest if reserved_block.reserved? && reserved_block.guest_id == guest.id
39
- raise Errors::BlocksAreOnHoldByOtherGuest if reserved_block.active_on_hold? && reserved_block.guest_id != guest.id
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 Errors::UnableToSaveReservedBlockRecord unless reserved_block.save
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 Errors::BlocksAreReservedByOtherGuest if reserved_block.reserved? && reserved_block.guest_id != guest.id
33
- raise Errors::BlocksAreReservedBySameGuest if reserved_block.reserved? && reserved_block.guest_id == guest.id
34
- raise Errors::BlocksAreOnHoldByOtherGuest if reserved_block.active_on_hold? && reserved_block.guest_id != guest.id
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 Errors::UnableToSaveReservedBlockRecord unless reserved_block.save
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
- required(:id_token).filled(:string)
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.update(
75
- completed_at: Time.zone.now,
76
- state: 'complete',
77
- payment_total: order.total,
78
- payment_state: 'paid'
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", admin_product_stock_management_variant_inventory_items(@product, variant.id),
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: #<%= @brand_color %>;
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', main_app.url_for("/a/#{line_item.qr_data}") %></div>
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>
@@ -1,7 +1,7 @@
1
1
  <div id="greeting-card-1">
2
2
  <div>
3
3
  <div class="header">
4
- <h1><%=@order.store.name %></h1>
4
+ <h1><%=@order.tenant.name %></h1>
5
5
  <% if @vendor_logo_url.present? %>
6
6
  <img src="<%= @vendor_logo_url %>" alt="Vendor Logo" class="icon-hang-meas">
7
7
  <% end %>
@@ -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 current_store.contact_phone.present? %>
10
+ <% if @order.store.contact_phone.present? %>
11
11
  <p class="two-columns_description">
12
- <%= link_to current_store.contact_phone, "tel:#{current_store.contact_phone}" %>
12
+ <%= link_to @order.store.contact_phone, "tel:#{@order.store.contact_phone}" %>
13
13
  </p>
14
14
  <% end %>
15
15
  </div>
@@ -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:
@@ -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]
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- VERSION = '2.2.1'.freeze
2
+ VERSION = '2.3.0-pre1'.freeze
3
3
 
4
4
  module_function
5
5
 
@@ -7,15 +7,12 @@ namespace :spree_cm_commissioner do
7
7
  total = scope.count
8
8
  puts "Users to backfill: #{total}"
9
9
 
10
- updated = 0
11
- scope.in_batches(of: batch_size) do |relation|
12
- relation.find_each do |user|
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("Updated #{updated} users")
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.2.1
4
+ version: 2.3.0.pre.pre1
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-24 00:00:00.000000000 Z
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: '0'
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
@@ -1,4 +0,0 @@
1
- module SpreeCmCommissioner
2
- class Seats::Errors::BlocksAreOnHoldByOtherGuest < StandardError
3
- end
4
- end
@@ -1,4 +0,0 @@
1
- module SpreeCmCommissioner
2
- class Seats::Errors::BlocksAreReservedByOtherGuest < StandardError
3
- end
4
- end
@@ -1,4 +0,0 @@
1
- module SpreeCmCommissioner
2
- class Seats::Errors::BlocksAreReservedBySameGuest < StandardError
3
- end
4
- end
@@ -1,4 +0,0 @@
1
- module SpreeCmCommissioner
2
- class Seats::Errors::UnableToSaveReservedBlockRecord < StandardError
3
- end
4
- end