Files
eminuxCRM/app/models/opportunity.rb
T
eminuxandCursor c4e5f289cf
CI / scan_ruby (push) Failing after 11m20s
CI / scan_js (push) Successful in 10m35s
CI / lint (push) Has been cancelled
Commit iniziale di eminuxCRM: CRM Rails con pipeline, campagne email e Docker.
Include autenticazione, progetti isolati, mail marketing HTML con SMTP, test A/B e editor WYSIWYG.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-17 23:01:48 +02:00

159 lines
4.3 KiB
Ruby

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