Module: SpreeCmCommissioner::ProductDecorator

Defined in:
app/models/spree_cm_commissioner/product_decorator.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.prepended(base) ⇒ Object



4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'app/models/spree_cm_commissioner/product_decorator.rb', line 4

def self.prepended(base)
  base.include SpreeCmCommissioner::ProductType
  base.include SpreeCmCommissioner::KycBitwise
  base.include SpreeCmCommissioner::Metafield
  base.include SpreeCmCommissioner::TenantUpdatable
  base.include SpreeCmCommissioner::ServiceType
  base.include SpreeCmCommissioner::ServiceRecommendations
  base.include SpreeCmCommissioner::Integrations::IntegrationMappable
  base.include SpreeCmCommissioner::StoreMetadata
  base.include SpreeCmCommissioner::HomepageSectionRelatableConcern

  base.delegate :is_open_dated, :is_open_dated?, to: :trip, allow_nil: true

  base.has_many :voting_sessions, class_name: 'SpreeCmCommissioner::VotingSession', foreign_key: :episode_id, dependent: :destroy

  base.has_many :variant_kind_option_types, -> { where(kind: :variant).order(:position) },
                through: :product_option_types, source: :option_type

  base.has_many :product_kind_option_types, -> { where(kind: :product).order(:position) },
                through: :product_option_types, source: :option_type

  base.has_many :promoted_option_types, -> { where(promoted: true).order(:position) },
                through: :product_option_types, source: :option_type

  base.has_many :option_values, through: :option_types
  base.has_many :prices_including_master, lambda {
                                            order('spree_variants.position, spree_variants.id, currency')
                                          }, source: :prices, through: :variants_including_master

  # after finish purchase an order, user must complete these steps
  base.has_many :product_completion_steps, class_name: 'SpreeCmCommissioner::ProductCompletionStep', dependent: :destroy

  base.has_one :default_state, through: :vendor
  base.has_one :google_wallet, class_name: 'SpreeCmCommissioner::GoogleWallet', dependent: :destroy

  base.has_many :complete_line_items, through: :classifications, source: :line_items
  base.has_many :inventory_items, through: :variants
  base.has_many :guests, through: :line_items

  base.has_many :product_places, class_name: 'SpreeCmCommissioner::ProductPlace', dependent: :destroy
  base.has_many :places, through: :product_places
  base.has_many :product_dynamic_fields, class_name: 'SpreeCmCommissioner::ProductDynamicField', dependent: :destroy
  base.has_many :dynamic_fields, through: :product_dynamic_fields, class_name: 'SpreeCmCommissioner::DynamicField'

  base.has_one :venue, -> { where(type: :venue) }, class_name: 'SpreeCmCommissioner::ProductPlace', dependent: :destroy

  base.accepts_nested_attributes_for :product_places, allow_destroy: true

  base.has_one :trip, class_name: 'SpreeCmCommissioner::Trip', dependent: :destroy
  base.has_one :service_calendar, as: :calendarable, class_name: 'SpreeCmCommissioner::ServiceCalendar', dependent: :destroy

  base.has_many :product_relations, class_name: 'SpreeCmCommissioner::ProductRelation', dependent: :destroy
  base.has_many :related_products, through: :product_relations

  base.belongs_to :event, class_name: 'Spree::Taxon', optional: true

  base.has_many :preview_roles, class_name: 'SpreeCmCommissioner::PreviewRole', as: :previewable
  base.has_many :industry_classifications, -> { joins(:taxon).where(spree_taxons: { kind: :industry }) }, class_name: 'Spree::Classification'
  base.has_many :industry_taxons, through: :industry_classifications, source: :taxon
  base.scope :visible_to, lambda { |user|
    publicly_available = where(status: :active, preview: false)

    if user
      # Resolved through associations — avoids hardcoding class names.
      taxon_type = reflect_on_association(:taxons).klass.polymorphic_name

      # IDs of products that have been explicitly assigned their own PreviewRole.
      products_with_own_roles = SpreeCmCommissioner::PreviewRole
                                .where(previewable_type: polymorphic_name)
                                .select(:previewable_id)

      # Path A: product has its own PreviewRole → user must hold that role directly.
      # Taxon membership is irrelevant for these products.
      via_product_role = where(status: :active, preview: true)
                         .where(id: products_with_own_roles)
                         .where(id: user.preview_roles.where(previewable_type: polymorphic_name).select(:previewable_id))

      # Path B: product has no own PreviewRole → inherit access from its event or taxons.
      # Accessible if the user holds a preview role for the product's event_id or any
      # taxon the product belongs to via the classifications join table.
      allowed_taxon_ids = user.preview_roles.where(previewable_type: taxon_type).select(:previewable_id)
      preview_no_own = where(status: :active, preview: true).where.not(id: products_with_own_roles)

      via_event_id = preview_no_own.where(event_id: allowed_taxon_ids)
      via_taxon_join = preview_no_own.joins(:taxons).where(spree_taxons: { id: allowed_taxon_ids })

      publicly_available
        .or(via_product_role)
        .or(where(id: via_event_id.select(:id)))
        .or(where(id: via_taxon_join.select(:id)))
    else
      publicly_available
    end
  }

  base.scope :min_price, lambda { |vendor|
    joins(:prices_including_master)
      .where(vendor_id: vendor.id, product_type: vendor.primary_product_type)
      .minimum('spree_prices.price').to_f
  }

  base.scope :max_price, lambda { |vendor|
    joins(:prices_including_master)
      .where(vendor_id: vendor.id, product_type: vendor.primary_product_type)
      .maximum('spree_prices.price').to_f
  }
  base.scope :subscribable, -> { where(subscribable: 1) }

  base.before_validation :set_event_id

  base.validate :validate_event_taxons, if: -> { taxons.event.present? }
  base.validate :validate_product_date, if: -> { available_on.present? && discontinue_on.present? }
  base.validate :product_type_unchanged, on: :update
  base.validates :commission_rate, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }, allow_nil: true

  base.whitelisted_ransackable_attributes = %w[description name slug discontinue_on status vendor_id short_name]

  base. :open_dated_validity_days, :integer, default: 90
  base. :enable_inventory_hold, :boolean, default: false

  base.after_update :update_variants_vendor_id, if: :saved_change_to_vendor_id?
  base.after_update :sync_event_id_to_children, if: :saved_change_to_event_id?

  base.enum purchasable_on: { both: 0, web: 1, app: 2 }, _prefix: true
  base.enum call_to_action: { buy_now: 0,
                              book_now: 1,
                              reserve: 2,
                              register: 3,
                              apply: 4,
                              get_ticket: 5,
                              secure_your_seat: 6,
                              join_the_event: 7,
                              request_to_book: 8
                              }

  base.belongs_to :tenant, class_name: 'SpreeCmCommissioner::Tenant'
  base.before_save :set_tenant

  # Create maintaining task to purge product related caches
  base.after_save { SpreeCmCommissioner::MaintenanceTasks::CacheInvalidation.pending.create_or_find_by(maintainable: self) }
end

Instance Method Details

#action_buttonObject



150
151
152
153
154
155
156
# File 'app/models/spree_cm_commissioner/product_decorator.rb', line 150

def action_button
  return :request_to_book if need_confirmation?
  return :reserve if free?
  return call_to_action.to_sym if call_to_action.present?

  :buy_now
end

#dynamic_fields_by_collection_phaseObject



166
167
168
169
170
171
172
# File 'app/models/spree_cm_commissioner/product_decorator.rb', line 166

def dynamic_fields_by_collection_phase
  {
    pre_registration: dynamic_fields.pre_registration.includes(:dynamic_field_options).order(:position),
    post_registration: dynamic_fields.post_registration.includes(:dynamic_field_options).order(:position),
    during_check_in: dynamic_fields.during_check_in.includes(:dynamic_field_options).order(:position)
  }
end

#free?Boolean

Returns:

  • (Boolean)


158
159
160
161
162
163
164
# File 'app/models/spree_cm_commissioner/product_decorator.rb', line 158

def free?
  return false if prices_including_master.empty?

  prices_including_master.all? do |master_price|
    master_price.price.zero? && (master_price.compare_at_price.nil? || master_price.compare_at_price.zero?)
  end
end

#required_dynamic_fields_completed?(guest, phase) ⇒ Boolean

Returns:

  • (Boolean)


174
175
176
177
178
179
180
# File 'app/models/spree_cm_commissioner/product_decorator.rb', line 174

def required_dynamic_fields_completed?(guest, phase)
  required_fields = dynamic_fields.where(data_fill_stage: phase)
  return true if required_fields.empty?

  filled_ids = guest.guest_dynamic_fields.where(dynamic_field_id: required_fields.select(:id)).pluck(:dynamic_field_id)
  (required_fields.pluck(:id) - filled_ids).empty?
end

#required_fields_for_guest(guest) ⇒ Object



182
183
184
185
186
# File 'app/models/spree_cm_commissioner/product_decorator.rb', line 182

def required_fields_for_guest(guest)
  dynamic_fields_by_collection_phase.transform_values do |fields|
    fields.select { |field| guest.guest_dynamic_fields.where(dynamic_field: field).empty? }
  end
end

#ticket_urlObject



146
147
148
# File 'app/models/spree_cm_commissioner/product_decorator.rb', line 146

def ticket_url
  "#{Spree::Store.default.formatted_url}/tickets/#{slug}"
end