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
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# 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.include SpreeCmCommissioner::PurchaseLimitable

  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. :enable_telegram_alert, :boolean, default: true
  base. :enable_send_receipt, :boolean, default: true
  base. :enable_social_sharing_email, :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.after_commit :trigger_event_price_range_refresh_for_event_change, 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) }

  # App opens the event page, not the ticket/product page, so the event's cache must be purged too.
  base.after_commit do
    [event_id, event_id_before_last_save].compact.uniq.each do |taxon_id|
      SpreeCmCommissioner::MaintenanceTasks::CacheInvalidation.pending.create_or_find_by(
        maintainable_type: 'Spree::Taxon',
        maintainable_id: taxon_id
      )
    end
  end
end

Instance Method Details

#action_buttonObject



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

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

#available?Boolean

override [spree_core] only checks active?/deleted?, so a scheduled product (available_on in the future) reports available even though it isn't purchasable yet. Since Variant#available? delegates here, this closes the gap for every caller (cart, checkout, Stock::AvailabilityChecker) in one place — mirrors VariantDecorator#discontinued?'s window check.

Returns:

  • (Boolean)


203
204
205
206
207
208
209
210
# File 'app/models/spree_cm_commissioner/product_decorator.rb', line 203

def available?
  return false unless active?
  return false if deleted?
  return false if discontinued?
  return false if available_on.present? && available_on > Time.current

  true
end

#free?Boolean

Returns:

  • (Boolean)


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

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

#ticket_urlObject



178
179
180
# File 'app/models/spree_cm_commissioner/product_decorator.rb', line 178

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