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.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/app/controllers/spree/api/chatrace/guests_controller.rb +3 -1
- data/app/controllers/spree/api/v2/storefront/line_items_controller.rb +24 -0
- data/app/controllers/spree/api/v2/tenant/line_items_controller.rb +10 -21
- data/app/errors/spree_cm_commissioner/seats/blocks_are_on_hold_by_other_guest_error.rb +12 -1
- data/app/errors/spree_cm_commissioner/seats/blocks_are_reserved_by_other_guest_error.rb +12 -1
- data/app/errors/spree_cm_commissioner/seats/unable_to_save_reserved_block_record_error.rb +12 -1
- data/app/jobs/spree_cm_commissioner/completion_steps/regenerate_for_line_items_job.rb +14 -0
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +10 -0
- data/app/models/concerns/spree_cm_commissioner/store_metadata.rb +20 -2
- data/app/models/spree_cm_commissioner/line_item_decorator.rb +26 -14
- data/app/models/spree_cm_commissioner/order_decorator.rb +1 -0
- data/app/models/spree_cm_commissioner/product_completion_step.rb +9 -32
- data/app/models/spree_cm_commissioner/product_completion_steps/social_entry_url.rb +92 -5
- data/app/models/spree_cm_commissioner/seats/blocks_canceler.rb +2 -1
- data/app/models/spree_cm_commissioner/seats/blocks_holder.rb +8 -3
- data/app/models/spree_cm_commissioner/seats/blocks_reserver.rb +8 -3
- data/app/serializers/spree/v2/storefront/order_serializer_decorator.rb +1 -1
- data/app/services/spree_cm_commissioner/completion_steps/mark_line_item_as_completed.rb +48 -0
- data/app/services/spree_cm_commissioner/completion_steps/regenerate_for_line_items.rb +53 -0
- data/config/locales/en.yml +3 -0
- data/config/locales/km.yml +3 -0
- data/config/routes.rb +6 -1
- data/lib/spree_cm_commissioner/test_helper/factories/product_completion_step_factory.rb +7 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +5 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 693c7855e8dc72dabdde0e8bf436e4dc9f4b7971bc1c09612ad8c454f49a6c79
         | 
| 4 | 
            +
              data.tar.gz: d9adba89a30ccd967d3472ca8e7ebf2b11b4fe1a9b1f1a898c765c1b67d642e6
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 4104850773bb75da44081c426992256bd439508c0144f7371d3bfebd890fa253996df33f6de1981b4e1ccb7db7050e43a2633b8ff0c763d5b364dc799ed28efd
         | 
| 7 | 
            +
              data.tar.gz: 7a76e698a0c34336f0d0805cdefadc84eac6c7a39e1a66d7792ab39ca50b4d3e7d04e1e1c1890462cdf8ae781e585df11636de56dca30efe1194391b935f1797
         | 
    
        data/Gemfile.lock
    CHANGED
    
    
| @@ -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 | 
            -
                       | 
| 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 | 
            -
                       | 
| 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 | 
            -
                         | 
| 25 | 
            -
             | 
| 26 | 
            +
                        result = SpreeCmCommissioner::CompletionSteps::MarkLineItemAsCompleted.call(
         | 
| 27 | 
            +
                          line_item: line_item,
         | 
| 28 | 
            +
                          position: step_position
         | 
| 29 | 
            +
                        )
         | 
| 26 30 |  | 
| 27 | 
            -
                        if  | 
| 28 | 
            -
                           | 
| 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( | 
| 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 | 
            -
                   | 
| 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 | 
            -
                   | 
| 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 | 
            -
                   | 
| 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:  | 
| 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:  | 
| 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  | 
| 201 | 
            -
                   | 
| 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 | 
            -
                 | 
| 8 | 
            -
             | 
| 9 | 
            -
                 | 
| 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  | 
| 34 | 
            -
                   | 
| 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 | 
            -
                  #  | 
| 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( | 
| 9 | 
            -
                    preferred_entry_point_link. | 
| 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?( | 
| 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 | 
            -
                     | 
| 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 | 
            -
                     | 
| 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 | 
            -
                     | 
| 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 | 
            -
                     | 
| 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 | 
| 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
         | 
    
        data/config/locales/en.yml
    CHANGED
    
    | @@ -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:
         | 
    
        data/config/locales/km.yml
    CHANGED
    
    | @@ -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
         | 
    
        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. | 
| 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- | 
| 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
         |