class Opportunity < ApplicationRecord include Auditable belongs_to :organization belongs_to :project belongs_to :assigned_user, class_name: "User", optional: true has_many :activities, dependent: :nullify has_many :tasks, dependent: :nullify validates :name, presence: true validates :pipeline_stage, inclusion: { in: Catalog::PIPELINE_STAGES.keys } validates :lost_reason, inclusion: { in: Catalog::LOST_REASONS.keys }, allow_blank: true validates :ab_variant, inclusion: { in: Catalog::AB_VARIANTS.keys }, allow_blank: true validates :send_status, inclusion: { in: Catalog::SEND_STATUSES.keys }, allow_blank: true validates :probability, numericality: { in: 0..100 }, allow_nil: true validates :estimated_value, numericality: { greater_than_or_equal_to: 0 }, allow_nil: true validate :lost_reason_required_when_lost before_validation :set_default_probability, on: :create before_validation :default_project_from_current, on: :create before_save :track_stage_change after_save :handle_stage_side_effects after_save :ensure_organization_in_project scope :open_stage, -> { where(pipeline_stage: Catalog::OPEN_PIPELINE_STAGES) } scope :won, -> { where(pipeline_stage: "won") } scope :lost, -> { where(pipeline_stage: "lost") } scope :in_stage, ->(stage) { where(pipeline_stage: stage) } scope :for_project, ->(project) { return none if project.nil? where(project_id: project.id) } def pipeline_stage_label Catalog.label_for(Catalog::PIPELINE_STAGES, pipeline_stage) end def lost_reason_label Catalog.label_for(Catalog::LOST_REASONS, lost_reason) end def ab_variant_label Catalog.label_for(Catalog::AB_VARIANTS, ab_variant) end def send_status_label Catalog.label_for(Catalog::SEND_STATUSES, send_status) end def open? Catalog::OPEN_PIPELINE_STAGES.include?(pipeline_stage) end def won? pipeline_stage == "won" end def lost? pipeline_stage == "lost" end def next_pending_task tasks.pending.order(:due_at).first end def days_in_current_stage anchor = stage_changed_at || created_at return 0 unless anchor ((Time.current - anchor) / 1.day).floor end def move_to_stage!(new_stage, lost_reason: nil, notes: nil, user: Current.user) attrs = { pipeline_stage: new_stage } attrs[:lost_reason] = lost_reason if lost_reason.present? attrs[:notes] = [self.notes, notes].compact_blank.join("\n") if notes.present? attrs[:probability] = Catalog::STAGE_PROBABILITIES.fetch(new_stage.to_s, probability) update!(attrs) create_stage_activity!(user) if saved_change_to_pipeline_stage? end private def set_default_probability self.probability ||= Catalog::STAGE_PROBABILITIES.fetch(pipeline_stage, 0) end def track_stage_change return unless pipeline_stage_changed? self.stage_changed_at = Time.current self.first_contacted_at ||= Time.current if pipeline_stage != "to_contact" case pipeline_stage when "won" self.won_at ||= Time.current self.lost_at = nil self.probability = 100 when "lost" self.lost_at ||= Time.current self.won_at = nil self.probability = 0 else self.won_at = nil self.lost_at = nil end end def handle_stage_side_effects return unless saved_change_to_pipeline_stage? organization.mark_as_active_customer! if won? end def lost_reason_required_when_lost return unless pipeline_stage == "lost" return if lost_reason.present? errors.add(:lost_reason, "è obbligatorio quando l'opportunità è persa") end def create_stage_activity!(user) return unless user type = won? ? "won" : lost? ? "lost" : "other" subject = if won? "Cliente #{product.presence || name}" elsif lost? "Perso: #{lost_reason_label}" else "Stage: #{pipeline_stage_label}" end activities.create!( activity_type: type, subject: subject, description: notes, happened_at: Time.current, user: user, organization: organization, created_by: user, updated_by: user ) end def default_project_from_current self.project ||= Current.project end def ensure_organization_in_project return unless project && organization organization.ensure_in_project!(project) end end