Include autenticazione, progetti isolati, mail marketing HTML con SMTP, test A/B e editor WYSIWYG. Co-authored-by: Cursor <cursoragent@cursor.com>
159 lines
4.3 KiB
Ruby
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
|