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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# 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

  # Inverse side of product_relations: relations where this product is the related_product
  # (e.g. trip add-ons point here).
  base.has_many :inverse_product_relations, class_name: 'SpreeCmCommissioner::ProductRelation',
                                            foreign_key: :related_product_id,
                                            dependent: :destroy

  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.scope :advertisements, lambda {
    advertisement
      .available
      .where('spree_products.available_on IS NULL OR spree_products.available_on <= CURRENT_TIMESTAMP')
      .where('spree_products.discontinue_on IS NULL OR spree_products.discontinue_on > CURRENT_TIMESTAMP')
  }

  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. :advertise_weight, :integer, default: 1
  base. :video_url, :string
  base. :skip_after_seconds, :integer, default: 5
  base. :duration_seconds, :integer, default: 30

  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



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

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



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

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)


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

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)


191
192
193
194
195
196
197
# File 'app/models/spree_cm_commissioner/product_decorator.rb', line 191

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



199
200
201
202
203
# File 'app/models/spree_cm_commissioner/product_decorator.rb', line 199

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



163
164
165
# File 'app/models/spree_cm_commissioner/product_decorator.rb', line 163

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