spree_cm_commissioner 2.3.0.pre.pre10 → 2.3.0.pre.pre12

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 (27) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/controllers/spree/api/chatrace/guests_controller.rb +3 -1
  4. data/app/controllers/spree/api/v2/storefront/line_items_controller.rb +24 -0
  5. data/app/controllers/spree/api/v2/tenant/line_items_controller.rb +10 -21
  6. data/app/errors/spree_cm_commissioner/seats/blocks_are_on_hold_by_other_guest_error.rb +12 -1
  7. data/app/errors/spree_cm_commissioner/seats/blocks_are_reserved_by_other_guest_error.rb +12 -1
  8. data/app/errors/spree_cm_commissioner/seats/unable_to_save_reserved_block_record_error.rb +12 -1
  9. data/app/jobs/spree_cm_commissioner/completion_steps/regenerate_for_line_items_job.rb +14 -0
  10. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +10 -0
  11. data/app/models/concerns/spree_cm_commissioner/store_metadata.rb +20 -2
  12. data/app/models/spree_cm_commissioner/line_item_decorator.rb +26 -14
  13. data/app/models/spree_cm_commissioner/order_decorator.rb +1 -0
  14. data/app/models/spree_cm_commissioner/product_completion_step.rb +9 -32
  15. data/app/models/spree_cm_commissioner/product_completion_steps/social_entry_url.rb +92 -5
  16. data/app/models/spree_cm_commissioner/seats/blocks_canceler.rb +2 -1
  17. data/app/models/spree_cm_commissioner/seats/blocks_holder.rb +8 -3
  18. data/app/models/spree_cm_commissioner/seats/blocks_reserver.rb +8 -3
  19. data/app/serializers/spree/v2/storefront/order_serializer_decorator.rb +1 -1
  20. data/app/services/spree_cm_commissioner/completion_steps/mark_line_item_as_completed.rb +48 -0
  21. data/app/services/spree_cm_commissioner/completion_steps/regenerate_for_line_items.rb +53 -0
  22. data/config/locales/en.yml +3 -0
  23. data/config/locales/km.yml +3 -0
  24. data/config/routes.rb +6 -1
  25. data/lib/spree_cm_commissioner/test_helper/factories/product_completion_step_factory.rb +7 -0
  26. data/lib/spree_cm_commissioner/version.rb +1 -1
  27. metadata +5 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3fd2c90bbaae526fa142b83741b54f05088e5f257445fcd1fb8f1848aa4e5962
4
- data.tar.gz: 5d0c437dc6da1c51576d06e8cd5cfb80bffc9faa79802aef8b7ac11dd3e590a0
3
+ metadata.gz: 693c7855e8dc72dabdde0e8bf436e4dc9f4b7971bc1c09612ad8c454f49a6c79
4
+ data.tar.gz: d9adba89a30ccd967d3472ca8e7ebf2b11b4fe1a9b1f1a898c765c1b67d642e6
5
5
  SHA512:
6
- metadata.gz: 6dafc95c2bd6d4733b9a2912faa109c1da958bf0531f8536382bebd34c526fef4fdebf73b8408afa54a9348c060abd5b05072c0f2b8b59baf555f96b2d51566e
7
- data.tar.gz: 8ab18278b04a39096144422f64957d73762fe60d268db4314d278b5121f59b007b748937b5864a7533b9bb18884573bf784143a1ef800b9014944ffbdf311d0e
6
+ metadata.gz: 4104850773bb75da44081c426992256bd439508c0144f7371d3bfebd890fa253996df33f6de1981b4e1ccb7db7050e43a2633b8ff0c763d5b364dc799ed28efd
7
+ data.tar.gz: 7a76e698a0c34336f0d0805cdefadc84eac6c7a39e1a66d7792ab39ca50b4d3e7d04e1e1c1890462cdf8ae781e585df11636de56dca30efe1194391b935f1797
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.3.0.pre.pre10)
37
+ spree_cm_commissioner (2.3.0.pre.pre12)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -18,9 +18,11 @@ module Spree
18
18
  guest.preferred_telegram_user_id = params[:telegram_user_id]
19
19
  guest.preferred_telegram_user_verified_at = DateTime.current
20
20
 
21
- if guest.save
21
+ # save & generate_completion_steps which will mark status as completed.
22
+ if guest.save && guest.line_item.generate_completion_steps
22
23
  render_serialized_payload { serialize_resource(guest) }
23
24
  else
25
+ guest.errors.merge!(guest.line_item.errors)
24
26
  render_error_payload(guest.errors)
25
27
  end
26
28
  end
@@ -16,6 +16,30 @@ module Spree
16
16
  render_serialized_payload { serialize_resource(line_item) }
17
17
  end
18
18
 
19
+ # PATCH /api/v2/storefront/line_items/1/try_mark_as_completed?order_token=:order_token&position=:position
20
+ # Attempts to mark a line item completion step as completed based on provided position.
21
+ def try_mark_as_completed
22
+ scope = if spree_current_user.nil?
23
+ Spree::LineItem.joins(:order).where(spree_orders: { token: params[:order_token] })
24
+ else
25
+ spree_current_user.line_items
26
+ end
27
+
28
+ line_item = scope.find(params[:id])
29
+ step_position = params.fetch(:position, 1).to_i
30
+
31
+ result = SpreeCmCommissioner::CompletionSteps::MarkLineItemAsCompleted.call(
32
+ line_item: line_item,
33
+ position: step_position
34
+ )
35
+
36
+ if result.success?
37
+ render_serialized_payload { serialize_resource(result.value) }
38
+ else
39
+ render_error_payload(result.error)
40
+ end
41
+ end
42
+
19
43
  def allowed_sort_attributes
20
44
  super << :to_date
21
45
  super << :from_date
@@ -18,31 +18,20 @@ module Spree
18
18
  render_serialized_payload { serialize_resource(line_item) }
19
19
  end
20
20
 
21
- def mark_as_completed # rubocop:disable Metrics/PerceivedComplexity
21
+ # TODO: refactor app to pass token here, it is not safe to pass id & update.
22
+ def mark_as_completed
22
23
  line_item = Spree::LineItem.find(params[:id])
24
+ step_position = params.fetch(:position, 1).to_i
23
25
 
24
- metadata = (line_item.public_metadata || {}).deep_transform_keys(&:to_s)
25
- steps = metadata['completion_steps']
26
+ result = SpreeCmCommissioner::CompletionSteps::MarkLineItemAsCompleted.call(
27
+ line_item: line_item,
28
+ position: step_position
29
+ )
26
30
 
27
- if steps.present? && steps.is_a?(Array)
28
- step = steps.find { |s| s['position'] == 1 } || steps[0]
29
-
30
- if step
31
- if step['action_url'].present?
32
- step['completed'] = true
33
- metadata['completion_steps'] = steps
34
- line_item.public_metadata = metadata
35
- line_item.save!
36
-
37
- render_serialized_payload { serialize_resource(line_item) }
38
- else
39
- render_error_payload('Action url is nil, cannot update to completed')
40
- end
41
- else
42
- render_error_payload('Step with position 1 not found')
43
- end
31
+ if result.success?
32
+ render_serialized_payload { serialize_resource(result.value) }
44
33
  else
45
- render_error_payload('completion_steps does not exist for this line_item')
34
+ render_error_payload(result.error)
46
35
  end
47
36
  end
48
37
 
@@ -1,8 +1,19 @@
1
1
  module SpreeCmCommissioner
2
2
  class Seats::BlocksAreOnHoldByOtherGuestError < StandardError
3
+ attr_reader :block_label
4
+
5
+ def initialize(block_label = nil)
6
+ @block_label = block_label
7
+ super()
8
+ end
9
+
3
10
  # override
4
11
  def message
5
- I18n.t('line_item.validation.blocks_are_on_hold_by_other_guest')
12
+ if block_label.present?
13
+ I18n.t('line_item.validation.blocks_are_on_hold_by_other_guest_with_label', label: block_label)
14
+ else
15
+ I18n.t('line_item.validation.blocks_are_on_hold_by_other_guest')
16
+ end
6
17
  end
7
18
  end
8
19
  end
@@ -1,8 +1,19 @@
1
1
  module SpreeCmCommissioner
2
2
  class Seats::BlocksAreReservedByOtherGuestError < StandardError
3
+ attr_reader :block_label
4
+
5
+ def initialize(block_label = nil)
6
+ @block_label = block_label
7
+ super()
8
+ end
9
+
3
10
  # override
4
11
  def message
5
- I18n.t('line_item.validation.blocks_are_reserved_by_other_guest')
12
+ if block_label.present?
13
+ I18n.t('line_item.validation.blocks_are_reserved_by_other_guest_with_label', label: block_label)
14
+ else
15
+ I18n.t('line_item.validation.blocks_are_reserved_by_other_guest')
16
+ end
6
17
  end
7
18
  end
8
19
  end
@@ -1,8 +1,19 @@
1
1
  module SpreeCmCommissioner
2
2
  class Seats::UnableToSaveReservedBlockRecordError < StandardError
3
+ attr_reader :block_label
4
+
5
+ def initialize(block_label = nil)
6
+ @block_label = block_label
7
+ super()
8
+ end
9
+
3
10
  # override
4
11
  def message
5
- I18n.t('line_item.validation.unable_to_save_reserved_block_record')
12
+ if block_label.present?
13
+ I18n.t('line_item.validation.unable_to_save_reserved_block_record_with_label', label: block_label)
14
+ else
15
+ I18n.t('line_item.validation.unable_to_save_reserved_block_record')
16
+ end
6
17
  end
7
18
  end
8
19
  end
@@ -0,0 +1,14 @@
1
+ module SpreeCmCommissioner
2
+ module CompletionSteps
3
+ class RegenerateForLineItemsJob < ApplicationJob
4
+ # Thin wrapper that regenerates completion steps for all line items
5
+ # of a product when a ProductCompletionStep is changed.
6
+ # ApplicationJob handles error logging via around_perform hook.
7
+ def perform(options = {})
8
+ product = Spree::Product.find(options[:product_id])
9
+
10
+ SpreeCmCommissioner::CompletionSteps::RegenerateForLineItems.call(product: product)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -10,6 +10,7 @@ module SpreeCmCommissioner
10
10
 
11
11
  state_machine.before_transition to: :complete, do: :request, if: :need_confirmation?
12
12
  state_machine.before_transition to: :complete, do: :generate_bib_number
13
+ state_machine.before_transition to: :complete, do: :generate_completion_steps!, if: :product_completion_steps_exist?
13
14
 
14
15
  state_machine.after_transition to: :complete, do: :precalculate_conversion
15
16
  state_machine.after_transition to: :complete, do: :notify_order_complete_app_notification_to_user, unless: :subscription?
@@ -71,6 +72,15 @@ module SpreeCmCommissioner
71
72
  end
72
73
  end
73
74
 
75
+ def product_completion_steps_exist?
76
+ product_completion_steps.any?
77
+ end
78
+
79
+ # This is called before complete, which is already wrapped by the transaction of unstock_inventory!
80
+ def generate_completion_steps!
81
+ line_items.each(&:generate_completion_steps!)
82
+ end
83
+
74
84
  def precalculate_conversion
75
85
  line_items.each do |item|
76
86
  SpreeCmCommissioner::ConversionPreCalculatorJob.perform_later(item.product_id)
@@ -37,16 +37,34 @@ module SpreeCmCommissioner
37
37
  module StoreMetadata
38
38
  extend ActiveSupport::Concern
39
39
 
40
+ # Custom JSON coder that gracefully handles corrupted JSON data
41
+ class ResilientJSONCoder
42
+ def self.dump(obj)
43
+ JSON.dump(obj)
44
+ end
45
+
46
+ def self.load(json)
47
+ return {} if json.blank?
48
+
49
+ begin
50
+ JSON.parse(json)
51
+ rescue JSON::ParserError, TypeError
52
+ # If JSON is corrupted, return empty hash instead of raising
53
+ {}
54
+ end
55
+ end
56
+ end
57
+
40
58
  class_methods do # rubocop:disable Metrics/BlockLength
41
59
  def store_private_metadata(key, type, default: nil)
42
- store :private_metadata, accessors: [key], coder: JSON
60
+ store :private_metadata, accessors: [key], coder: ResilientJSONCoder
43
61
 
44
62
  define_metadata_reader(:private_metadata, key, type, default)
45
63
  define_metadata_validation(:private_metadata, key, type)
46
64
  end
47
65
 
48
66
  def store_public_metadata(key, type, default: nil)
49
- store :public_metadata, accessors: [key], coder: JSON
67
+ store :public_metadata, accessors: [key], coder: ResilientJSONCoder
50
68
 
51
69
  define_metadata_reader(:public_metadata, key, type, default)
52
70
  define_metadata_validation(:public_metadata, key, type)
@@ -26,8 +26,6 @@ module SpreeCmCommissioner
26
26
 
27
27
  base.before_save :update_vendor_id
28
28
 
29
- base.after_commit :persist_completion_steps_to_public_metadata, on: :create
30
-
31
29
  base.before_create :add_due_date, if: :subscription?
32
30
 
33
31
  base.validate :ensure_not_exceed_max_quantity_per_order, if: -> { variant&.max_quantity_per_order.present? }
@@ -39,6 +37,8 @@ module SpreeCmCommissioner
39
37
  base.delegate :delivery_required?, :high_demand,
40
38
  to: :variant
41
39
 
40
+ base.store_public_metadata :completion_steps, :array
41
+
42
42
  base.accepts_nested_attributes_for :guests, allow_destroy: true
43
43
 
44
44
  def base.json_api_columns
@@ -66,6 +66,7 @@ module SpreeCmCommissioner
66
66
 
67
67
  def self.include_modules(base)
68
68
  base.include Spree::Core::NumberGenerator.new(prefix: 'L')
69
+ base.include SpreeCmCommissioner::StoreMetadata
69
70
  base.include SpreeCmCommissioner::LineItemDurationable
70
71
  base.include SpreeCmCommissioner::LineItemsFilterScope
71
72
  base.include SpreeCmCommissioner::LineItemGuestsConcern
@@ -197,8 +198,29 @@ module SpreeCmCommissioner
197
198
  "#{order.number}-#{order.token}-L#{id}"
198
199
  end
199
200
 
200
- def completion_steps
201
- public_metadata[:completion_steps] || []
201
+ def generate_completion_steps
202
+ generate_completion_steps!
203
+ true
204
+ rescue StandardError => e
205
+ CmAppLogger.error(
206
+ label: "#{self.class.name}#generate_completion_steps",
207
+ data: {
208
+ line_item_id: id,
209
+ order_id: order_id,
210
+ error_class: e.class.name,
211
+ error_message: e.message,
212
+ backtrace: e.backtrace&.first(10)&.join("\n")
213
+ }
214
+ )
215
+ errors.add(:completion_steps, e.message)
216
+ false
217
+ end
218
+
219
+ def generate_completion_steps!
220
+ self.completion_steps = product_completion_steps.map do |completion_step|
221
+ completion_step.construct_hash(line_item: self)
222
+ end
223
+ save!
202
224
  end
203
225
 
204
226
  # override
@@ -217,16 +239,6 @@ module SpreeCmCommissioner
217
239
 
218
240
  private
219
241
 
220
- def persist_completion_steps_to_public_metadata
221
- steps = product_completion_steps.order(:position).map do |pcs|
222
- pcs.construct_hash(line_item: self).except(:completed).merge(
223
- id: pcs.id,
224
- completed: false
225
- )
226
- end
227
- update_column(:public_metadata, (public_metadata || {}).merge(completion_steps: steps)) # rubocop:disable Rails/SkipsModelValidations
228
- end
229
-
230
242
  def ensure_not_exceed_max_quantity_per_order
231
243
  errors.add(:quantity, I18n.t('line_item.validation.exceeded_max_quantity_per_order')) if quantity > variant.max_quantity_per_order
232
244
  end
@@ -57,6 +57,7 @@ module SpreeCmCommissioner
57
57
  base.has_many :blocks, through: :guests, class_name: 'SpreeCmCommissioner::Block', source: :block
58
58
  base.has_many :reserved_blocks, through: :guests, class_name: 'SpreeCmCommissioner::ReservedBlock'
59
59
  base.has_many :guest_card_classes, class_name: 'SpreeCmCommissioner::GuestCardClass', through: :variants
60
+ base.has_many :product_completion_steps, class_name: 'SpreeCmCommissioner::ProductCompletionStep', through: :line_items
60
61
 
61
62
  base.delegate :customer, to: :user, allow_nil: true
62
63
 
@@ -4,9 +4,10 @@ module SpreeCmCommissioner
4
4
 
5
5
  belongs_to :product, class_name: '::Spree::Product', optional: false
6
6
 
7
- after_save :propagate_completion_steps_to_line_items
8
-
9
- after_destroy_commit :propagate_completion_steps_to_line_items
7
+ # When a completion step is changed, regenerate completion steps for all line items
8
+ # of the product so they reflect the latest step configuration.
9
+ after_destroy :regenerate_line_items_completion_steps
10
+ after_save :regenerate_line_items_completion_steps
10
11
 
11
12
  def action_url_for(_line_item)
12
13
  nil
@@ -17,6 +18,8 @@ module SpreeCmCommissioner
17
18
  end
18
19
 
19
20
  def construct_hash(line_item:)
21
+ existing_data = line_item.completion_steps&.find { |step| step['position'].to_i == position }
22
+
20
23
  {
21
24
  title: title,
22
25
  type: type&.underscore,
@@ -24,41 +27,15 @@ module SpreeCmCommissioner
24
27
  description: description,
25
28
  action_label: action_label,
26
29
  action_url: action_url_for(line_item),
30
+ completed_at: existing_data&.dig('completed_at'),
27
31
  completed: completed?(line_item)
28
32
  }
29
33
  end
30
34
 
31
35
  private
32
36
 
33
- def propagate_completion_steps_to_line_items
34
- product.line_items.find_each do |line_item|
35
- propagate_to_line_item(line_item)
36
- end
37
- end
38
-
39
- def propagate_to_line_item(line_item)
40
- current_metadata = line_item.reload.public_metadata || {}
41
- stored_steps = Array(current_metadata[:completion_steps])
42
-
43
- fresh_steps = product.product_completion_steps.order(:position).map do |pcs|
44
- base_attrs = pcs.construct_hash(line_item: line_item).except(:completed)
45
-
46
- previous =
47
- stored_steps.find { |s| s[:id] == pcs.id } ||
48
- stored_steps.find { |s| s[:type] == pcs.type&.underscore && s[:position] == pcs.position }
49
-
50
- completed_flag = previous&.dig(:completed) ? true : false
51
-
52
- base_attrs.merge(
53
- id: pcs.id,
54
- completed: completed_flag
55
- )
56
- end
57
-
58
- line_item.update_column( # rubocop:disable Rails/SkipsModelValidations
59
- :public_metadata,
60
- current_metadata.merge(completion_steps: fresh_steps)
61
- )
37
+ def regenerate_line_items_completion_steps
38
+ SpreeCmCommissioner::CompletionSteps::RegenerateForLineItemsJob.perform_later(product_id: product_id)
62
39
  end
63
40
  end
64
41
  end
@@ -1,16 +1,103 @@
1
1
  module SpreeCmCommissioner
2
2
  module ProductCompletionSteps
3
3
  class SocialEntryUrl < ProductCompletionStep
4
- # eg. https://t.me/ThePlatformKHBot?start=bookmeplus
4
+ # Template format examples:
5
+ # - https://t.me/ThePlatformKHBot?start={order.number}
6
+ # - https://example.com?guest_name={guests[0].first_name}&phone={guests[0].phone_number}
7
+ # - https://example.com?line_item_id={line_item.id}&quantity={line_item.quantity}
5
8
  preference :entry_point_link, :string
6
9
 
10
+ # Allowed fields per object (excludes private/reference fields like *_id, *_by_id, metadata, etc.)
11
+ ORDER_ALLOWED_FIELDS = %w[
12
+ qr_data
13
+ number
14
+ state
15
+ email
16
+ total
17
+ item_total
18
+ created_at
19
+ completed_at
20
+ token
21
+ phone_number
22
+ country_code
23
+ currency
24
+ item_count
25
+ channel
26
+ ].freeze
27
+
28
+ LINE_ITEM_ALLOWED_FIELDS = %w[
29
+ qr_data
30
+ quantity
31
+ price
32
+ from_date
33
+ to_date
34
+ number
35
+ remark
36
+ created_at
37
+ updated_at
38
+ ].freeze
39
+
40
+ GUEST_ALLOWED_FIELDS = %w[
41
+ qr_data
42
+ first_name
43
+ last_name
44
+ full_name
45
+ dob
46
+ gender
47
+ age
48
+ email
49
+ phone_number
50
+ address
51
+ seat_number
52
+ bib_number
53
+ bib_prefix
54
+ bib_index
55
+ formatted_bib_number
56
+ token
57
+ social_contact
58
+ country_code
59
+ emergency_contact
60
+ expectation
61
+ other_occupation
62
+ other_organization
63
+ entry_type
64
+ upload_later
65
+ data_fill_stage_phase
66
+ created_at
67
+ updated_at
68
+ ].freeze
69
+
7
70
  # override
8
- def action_url_for(_line_item)
9
- preferred_entry_point_link.presence
71
+ def action_url_for(line_item)
72
+ return nil if preferred_entry_point_link.blank?
73
+
74
+ url = preferred_entry_point_link.dup
75
+
76
+ # Replace order placeholders: {order.field_name}
77
+ line_item.order.attributes.each do |field, value|
78
+ url.gsub!("{order.#{field}}", value.to_s) if ORDER_ALLOWED_FIELDS.include?(field)
79
+ end
80
+
81
+ # Replace line_item placeholders: {line_item.field_name}
82
+ line_item.attributes.each do |field, value|
83
+ url.gsub!("{line_item.#{field}}", value.to_s) if LINE_ITEM_ALLOWED_FIELDS.include?(field)
84
+ end
85
+
86
+ # Replace guest placeholders: {guests[index].field_name}
87
+ line_item.guests.each_with_index do |guest, index|
88
+ guest.attributes.each do |field, value|
89
+ url.gsub!("{guests[#{index}].#{field}}", value.to_s) if GUEST_ALLOWED_FIELDS.include?(field)
90
+ end
91
+ end
92
+
93
+ url
10
94
  end
11
95
 
12
- def completed?(_line_item)
13
- false
96
+ def completed?(line_item)
97
+ return false if line_item.completion_steps.blank?
98
+
99
+ step_data = line_item.completion_steps.find { |step| step['position'].to_s == position.to_s }
100
+ step_data&.dig('completed_at').present?
14
101
  end
15
102
  end
16
103
  end
@@ -16,12 +16,13 @@ module SpreeCmCommissioner
16
16
 
17
17
  ActiveRecord::Base.transaction do
18
18
  order.reserved_blocks.each do |reserved_block|
19
+ block_label = reserved_block.block.label
19
20
  reserved_block.assign_attributes(
20
21
  status: :canceled,
21
22
  expired_at: nil,
22
23
  updated_by: @cancel_by
23
24
  )
24
- raise SpreeCmCommissioner::Seats::UnableToSaveReservedBlockRecordError unless reserved_block.save
25
+ raise SpreeCmCommissioner::Seats::UnableToSaveReservedBlockRecordError, block_label unless reserved_block.save
25
26
  end
26
27
  end
27
28
  end
@@ -39,10 +39,15 @@ module SpreeCmCommissioner
39
39
 
40
40
  def hold_specific_block!(inventory_item, guest)
41
41
  reserved_block = SpreeCmCommissioner::ReservedBlock.find_or_initialize_by(inventory_item: inventory_item, block: guest.block)
42
+ block_label = guest.block.label
42
43
 
43
- raise SpreeCmCommissioner::Seats::BlocksAreReservedByOtherGuestError if reserved_block.reserved? && reserved_block.guest_id != guest.id
44
+ if reserved_block.reserved? && reserved_block.guest_id != guest.id
45
+ raise SpreeCmCommissioner::Seats::BlocksAreReservedByOtherGuestError, block_label
46
+ end
44
47
  raise SpreeCmCommissioner::Seats::BlocksAreReservedBySameGuestError if reserved_block.reserved? && reserved_block.guest_id == guest.id
45
- raise SpreeCmCommissioner::Seats::BlocksAreOnHoldByOtherGuestError if reserved_block.active_on_hold? && reserved_block.guest_id != guest.id
48
+ if reserved_block.active_on_hold? && reserved_block.guest_id != guest.id
49
+ raise SpreeCmCommissioner::Seats::BlocksAreOnHoldByOtherGuestError, block_label
50
+ end
46
51
 
47
52
  reserved_block.assign_attributes(
48
53
  status: :on_hold,
@@ -52,7 +57,7 @@ module SpreeCmCommissioner
52
57
  updated_by: @hold_by
53
58
  )
54
59
 
55
- raise SpreeCmCommissioner::Seats::UnableToSaveReservedBlockRecordError unless reserved_block.save
60
+ raise SpreeCmCommissioner::Seats::UnableToSaveReservedBlockRecordError, block_label unless reserved_block.save
56
61
 
57
62
  reserved_block
58
63
  end
@@ -30,10 +30,15 @@ module SpreeCmCommissioner
30
30
 
31
31
  def reserve_specific_block!(inventory_item, guest)
32
32
  reserved_block = SpreeCmCommissioner::ReservedBlock.find_or_initialize_by(inventory_item_id: inventory_item.id, block_id: guest.block_id)
33
+ block_label = guest.block.label
33
34
 
34
- raise SpreeCmCommissioner::Seats::BlocksAreReservedByOtherGuestError if reserved_block.reserved? && reserved_block.guest_id != guest.id
35
+ if reserved_block.reserved? && reserved_block.guest_id != guest.id
36
+ raise SpreeCmCommissioner::Seats::BlocksAreReservedByOtherGuestError, block_label
37
+ end
35
38
  raise SpreeCmCommissioner::Seats::BlocksAreReservedBySameGuestError if reserved_block.reserved? && reserved_block.guest_id == guest.id
36
- raise SpreeCmCommissioner::Seats::BlocksAreOnHoldByOtherGuestError if reserved_block.active_on_hold? && reserved_block.guest_id != guest.id
39
+ if reserved_block.active_on_hold? && reserved_block.guest_id != guest.id
40
+ raise SpreeCmCommissioner::Seats::BlocksAreOnHoldByOtherGuestError, block_label
41
+ end
37
42
 
38
43
  # mark the block as reserved if not on_hold or lock by anyone but already expired
39
44
  reserved_block.assign_attributes(
@@ -44,7 +49,7 @@ module SpreeCmCommissioner
44
49
  updated_by: @reserve_by
45
50
  )
46
51
 
47
- raise SpreeCmCommissioner::Seats::UnableToSaveReservedBlockRecordError unless reserved_block.save
52
+ raise SpreeCmCommissioner::Seats::UnableToSaveReservedBlockRecordError, block_label unless reserved_block.save
48
53
  end
49
54
  end
50
55
  end
@@ -4,7 +4,7 @@ module Spree
4
4
  module OrderSerializerDecorator
5
5
  def self.prepended(base)
6
6
  base.attribute :has_incomplete_guest_info do |order|
7
- order.user.public_metadata['has_incomplete_guest_info'] || false
7
+ order.user&.public_metadata&.[]('has_incomplete_guest_info') || false
8
8
  end
9
9
 
10
10
  base.attribute :guest_info_status do |order|
@@ -0,0 +1,48 @@
1
+ module SpreeCmCommissioner
2
+ module CompletionSteps
3
+ class MarkLineItemAsCompleted
4
+ prepend ::Spree::ServiceModule::Base
5
+
6
+ def call(line_item:, position: 1)
7
+ ApplicationRecord.transaction do
8
+ validate_completion_steps!(line_item)
9
+ validate_step_exists!(line_item, position)
10
+ validate_action_url!(line_item, position)
11
+
12
+ # this will set completed_at timestamp to the step in line item
13
+ mark_step_completed!(line_item, position)
14
+
15
+ # regenerate completion steps to check whether is consider completed.
16
+ # because some steps, just completed at is not enough.
17
+ line_item.generate_completion_steps!
18
+
19
+ success(line_item)
20
+ end
21
+ rescue StandardError => e
22
+ failure(nil, e.message)
23
+ end
24
+
25
+ private
26
+
27
+ def validate_completion_steps!(line_item)
28
+ raise 'Completion steps not found for this line item' if line_item.completion_steps.blank?
29
+ end
30
+
31
+ def validate_step_exists!(line_item, position)
32
+ raise "Step at position #{position} not found" if step_data(line_item, position).blank?
33
+ end
34
+
35
+ def validate_action_url!(line_item, position)
36
+ raise 'Action URL is missing for this step' if step_data(line_item, position)['action_url'].blank?
37
+ end
38
+
39
+ def mark_step_completed!(line_item, position)
40
+ step_data(line_item, position)['completed_at'] = Time.current.iso8601
41
+ end
42
+
43
+ def step_data(line_item, position)
44
+ line_item.completion_steps.find { |step| step['position'].to_s == position.to_s }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,53 @@
1
+ module SpreeCmCommissioner
2
+ module CompletionSteps
3
+ class RegenerateForLineItems
4
+ prepend ::Spree::ServiceModule::Base
5
+
6
+ # Regenerates completion steps for all completed line items of a product.
7
+ # Called when a ProductCompletionStep is created, updated, or destroyed.
8
+ def call(product:)
9
+ line_items = product.line_items.complete
10
+
11
+ CmAppLogger.log(
12
+ label: 'SpreeCmCommissioner::CompletionSteps::RegenerateForLineItems#call Starting',
13
+ data: {
14
+ product_id: product.id,
15
+ line_items_count: line_items.count
16
+ }
17
+ )
18
+
19
+ regenerated_count = 0
20
+ failed_count = 0
21
+
22
+ line_items.find_each do |line_item|
23
+ line_item.generate_completion_steps!
24
+ regenerated_count += 1
25
+ rescue StandardError => e
26
+ CmAppLogger.error(
27
+ label: 'SpreeCmCommissioner::CompletionSteps::RegenerateForLineItems#call Line item regeneration failed',
28
+ data: {
29
+ product_id: product.id,
30
+ line_item_id: line_item.id,
31
+ error_class: e.class.name,
32
+ error_message: e.message
33
+ }
34
+ )
35
+ failed_count += 1
36
+ end
37
+
38
+ CmAppLogger.log(
39
+ label: 'SpreeCmCommissioner::CompletionSteps::RegenerateForLineItems#call Completed',
40
+ data: {
41
+ product_id: product.id,
42
+ regenerated_count: regenerated_count,
43
+ failed_count: failed_count
44
+ }
45
+ )
46
+
47
+ success(regenerated_count: regenerated_count, failed_count: failed_count)
48
+ rescue StandardError => e
49
+ failure(nil, e.message)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -124,9 +124,12 @@ en:
124
124
  exceeded_max_quantity_per_order: "Exceeded maximum quantity per order"
125
125
  seats_are_required: "Seats are required for all guests"
126
126
  blocks_are_reserved_by_other_guest: "Seats were recently reserved by another guest"
127
+ blocks_are_reserved_by_other_guest_with_label: "Seat %{label} was recently reserved by another guest"
127
128
  blocks_are_on_hold_by_other_guest: "Seats were recently put on hold by another guest"
129
+ blocks_are_on_hold_by_other_guest_with_label: "Seat %{label} was recently put on hold by another guest"
128
130
  blocks_are_reserved_by_same_guest: "Seats were recently reserved by this guest"
129
131
  unable_to_save_reserved_block_record: "Unable to save seat reservation"
132
+ unable_to_save_reserved_block_record_with_label: "Unable to save reservation for seat %{label}"
130
133
 
131
134
  vectors:
132
135
  icons:
@@ -118,9 +118,12 @@ km:
118
118
  exceeded_max_quantity_per_order: "លើសពីបរិមាណអតិបរមាក្នុងមួយការបញ្ជាទិញ"
119
119
  seats_are_required: "ត្រូវការលេខកៅអីសម្រាប់ភ្ញៀវទាំងអស់"
120
120
  blocks_are_reserved_by_other_guest: "កៅអីត្រូវបានកក់ថ្មីៗដោយភ្ញៀវផ្សេងទៀត"
121
+ blocks_are_reserved_by_other_guest_with_label: "កៅអី %{label} ត្រូវបានកក់ថ្មីៗដោយភ្ញៀវផ្សេងទៀត"
121
122
  blocks_are_on_hold_by_other_guest: "កៅអីត្រូវបានផ្អាកថ្មីៗដោយភ្ញៀវផ្សេងទៀត"
123
+ blocks_are_on_hold_by_other_guest_with_label: "កៅអី %{label} ត្រូវបានផ្អាកថ្មីៗដោយភ្ញៀវផ្សេងទៀត"
122
124
  blocks_are_reserved_by_same_guest: "កៅអីត្រូវបានកក់ថ្មីៗដោយភ្ញៀវនេះ"
123
125
  unable_to_save_reserved_block_record: "មិនអាចរក្សាទុកការកក់កៅអីបានទេ"
126
+ unable_to_save_reserved_block_record_with_label: "មិនអាចរក្សាទុកការកក់សម្រាប់កៅអី %{label} បានទេ"
124
127
 
125
128
  subscription:
126
129
  validation:
data/config/routes.rb CHANGED
@@ -595,7 +595,12 @@ Spree::Core::Engine.add_routes do
595
595
  resources :variants, only: %i[index show], module: :accommodations
596
596
  end
597
597
 
598
- resources :line_items, only: %i[index show]
598
+ resources :line_items, only: %i[index show] do
599
+ member do
600
+ patch :try_mark_as_completed
601
+ end
602
+ end
603
+
599
604
  resources :account_checker
600
605
  resource :account_recovers, only: [:update]
601
606
 
@@ -8,4 +8,11 @@ FactoryBot.define do
8
8
  preferred_entry_point_link { 'https://t.me/ThePlatformKHBot?start=bookmeplus' }
9
9
  end
10
10
  end
11
+
12
+ factory :cm_social_entry_url_product_completion_step, class: SpreeCmCommissioner::ProductCompletionSteps::SocialEntryUrl do
13
+ title { 'Must complete this step' }
14
+ description { 'Click open now to complete!' }
15
+ action_label { 'Open' }
16
+ preferred_entry_point_link { 'https://www.mywebsite?order_number={order.number}&line_item_number={line_item.number}' }
17
+ end
11
18
  end
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- VERSION = '2.3.0-pre10'.freeze
2
+ VERSION = '2.3.0-pre12'.freeze
3
3
 
4
4
  module_function
5
5
 
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.3.0.pre.pre10
4
+ version: 2.3.0.pre.pre12
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-30 00:00:00.000000000 Z
11
+ date: 2025-10-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: spree
@@ -1289,6 +1289,7 @@ files:
1289
1289
  - app/jobs/spree_cm_commissioner/application_job_decorator.rb
1290
1290
  - app/jobs/spree_cm_commissioner/application_unique_job.rb
1291
1291
  - app/jobs/spree_cm_commissioner/chatrace_order_creator_job.rb
1292
+ - app/jobs/spree_cm_commissioner/completion_steps/regenerate_for_line_items_job.rb
1292
1293
  - app/jobs/spree_cm_commissioner/conversion_pre_calculator_job.rb
1293
1294
  - app/jobs/spree_cm_commissioner/customer_content_notification_creator_job.rb
1294
1295
  - app/jobs/spree_cm_commissioner/customer_notification_cron_job.rb
@@ -1917,6 +1918,8 @@ files:
1917
1918
  - app/services/spree_cm_commissioner/cart/remove_guest.rb
1918
1919
  - app/services/spree_cm_commissioner/checkout/advance_decorator.rb
1919
1920
  - app/services/spree_cm_commissioner/checkout/update_decorator.rb
1921
+ - app/services/spree_cm_commissioner/completion_steps/mark_line_item_as_completed.rb
1922
+ - app/services/spree_cm_commissioner/completion_steps/regenerate_for_line_items.rb
1920
1923
  - app/services/spree_cm_commissioner/exports/export_guest_csv_service.rb
1921
1924
  - app/services/spree_cm_commissioner/exports/export_order_csv_service.rb
1922
1925
  - app/services/spree_cm_commissioner/feed.rb