Skip to content

Instantly share code, notes, and snippets.

@jasonswett
Created November 24, 2013 18:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jasonswett/7630490 to your computer and use it in GitHub Desktop.
Save jasonswett/7630490 to your computer and use it in GitHub Desktop.
A fat model
class Appointment < ActiveRecord::Base
include ActionView::Helpers::NumberHelper
default_scope :order => "start_time asc"
scope :in_date_range, lambda { |start_date, end_date| self.in_date_range(start_date, end_date) }
scope :for_day, lambda { |date| in_date_range(date, date) }
scope :for_today, lambda { for_day(Time.zone.today) }
scope :for_tomorrow, lambda { for_day(Time.zone.today + 1.day) }
scope :not_time_blocks, joins(:time_block_type).where("code = 'APPOINTMENT'")
scope :checked_out, joins(:payments)
scope :past, lambda { where("start_time <= ?", Time.zone.now) }
scope :eager, lambda {
includes(
:client,
:stylist,
:services,
:appointment_services,
:products,
:payments,
:time_block_type
).includes(
appointment_services: [:service]
).includes(
appointment_products: [:product]
)
}
scope :that_need_reminders_today, lambda {
for_tomorrow.not_time_blocks.joins(:client).where("wants_email_reminders = true")
}
attr_accessor :only_update_self, :should_save_future
has_many :appointment_services, :dependent => :destroy
has_many :services, :through => :appointment_services
has_many :appointment_products, :dependent => :destroy
has_many :products, :through => :appointment_products
has_many :payments, :dependent => :destroy
has_many :transaction_items
belongs_to :client
belongs_to :stylist
belongs_to :time_block_type
accepts_nested_attributes_for :client, :stylist, :time_block_type
attr_accessible :client_attributes, :time_block_type_attributes, :start_time,
:client, :time_block_type, :start_time_time,
:stylist, :length, :start_time_ymd,
:stylist_id, :is_cancelled, :tip,
:notes, :should_save_future, :recurrence_rule_hash,
:can_repeat, :repeats_every_how_many_weeks
before_save :make_sure_length_makes_sense
before_save :generate_recurrence_hash_if_needed
before_save :drop_out_of_recurrence_if_needed
before_save :unrepeat_if_needed
after_initialize :init
validates_presence_of :start_time_time, :message => "Looks like you forgot the appointment start time."
validates_presence_of :start_time_ymd, :message => "Please choose a date for the appointment."
validates_presence_of :client
# TODO: Break these validators out into separate functions.
# (or use validates_timeliness)
validates_each :start_time_time do |model, attr, value|
begin
Time.zone.parse(value)
rescue
model.errors.add(attr, "Sorry, we can't understand \"#{value}\" as a time.")
end
end
validates_each :start_time_ymd do |model, attr, value|
begin
Date.strptime(value, "%m/%d/%Y")
rescue
model.errors.add(attr, "Sorry, we can't understand \"#{value}\" as a date.")
end
end
validates_each :start_time do |model, attr, value|
if model.stylist
message = %Q(
Sorry, there's already an appointment for #{model.stylist.name}
on #{model.start_time_ymd} at #{model.start_time_time},
and you can't schedule two appointments for the same stylist at the same time.
)
model.errors.add(attr, message) if model.conflicting_with_another_appointment? and !model.is_cancelled
end
end
def self.save_from_params(params)
a = params[:id] ? self.find(params[:id]) : self.new
a.attributes = {
:time_block_type => TimeBlockType.find_by_code(params[:appointment][:time_block_type_code]),
:notes => params[:appointment][:notes],
:stylist_id => params[:appointment][:stylist_id],
:is_cancelled => params[:appointment][:is_cancelled],
:tip => params[:appointment][:tip],
}
if a.time_block_type_code != "APPOINTMENT"
a.client = Client.no_client
a.length = params[:appointment][:length]
else
a.client = self.build_client(params, a.stylist.salon)
end
a.set_time(params[:appointment][:start_time_time], params[:appointment][:start_time_ymd])
a.set_repeat_logic(params)
a.save
a.set_payments_from_json_string(params[:serialized_payments])
a.set_services_and_products_from_json_string(params[:serialized_products_and_services])
a.record_transactions
if !a.new_record?
a.reload.generate_recurrence_hash_if_needed
a.reload.save_future
end
a
end
def set_repeat_logic(params)
if params[:appointment][:repeats]
self.repeats_every_how_many_weeks = params[:appointment][:repeats_every_how_many_weeks]
else
self.repeats_every_how_many_weeks = 0
end
# Only update self as opposed to update all appointments in series.
if params[:only_update_self] == "true"
self.only_update_self = true
else
self.only_update_self = false
end
end
def record_transactions
transaction_items.destroy_all
if paid_for?
save_service_transaction_items
save_product_transaction_items
save_tip_transaction_item
end
end
def save_service_transaction_items
appointment_services.reload.each { |s| s.save_transaction_item(self.id) }
end
def save_product_transaction_items
appointment_products.reload.each { |p| p.save_transaction_item(self.id) }
end
def save_tip_transaction_item
TransactionItem.create!(
:appointment_id => self.id,
:stylist_id => self.stylist_id,
:label => "Tip",
:price => self.tip,
:transaction_item_type_id => TransactionItemType.find_or_create_by_code("TIP").id
)
end
def self.build_client(params, salon)
if params[:client_id] != ''
client = Client.find(params[:client_id])
else
client = Client.find_or_create_by_name_and_phone_and_salon_id(params[:client_name], params[:appointment][:client][:phone], salon.id)
end
client.phone = params[:appointment][:client][:phone]
client.notes = params[:appointment][:client][:notes]
client.email = params[:appointment][:client][:email]
client.wants_email_reminders = params[:appointment][:client][:wants_email_reminders]
client.address = Address.find_or_create_from_params({
:id => client.address_id,
:line1 => params[:appointment][:address][:line1],
:line2 => params[:appointment][:address][:line2],
:city => params[:appointment][:address][:city],
:state_id => params[:appointment][:address][:state_id],
:zip => params[:appointment][:address][:zip],
})
client
end
def init
self.should_save_future = true
if new_record?
self.time_block_type = TimeBlockType.find_or_create_by_code_and_label("APPOINTMENT", "Appointment")
end
end
def serializable_hash(options={})
options = {
:methods => [
"notes",
"client",
"services",
"products",
"start_time_ymd",
"start_time_time",
"calculated_length",
"day_of_week_index",
"services_with_info",
"products_with_info",
"payments_with_info",
"stylist_name_index",
"time_block_type_code",
"client_name_that_fits",
"minutes_since_midnight",
"generous_calculated_length",
]}.update(options)
super(options)
end
def serialized_products_and_services
services.collect { |service| service.serializable_hash }.to_json
end
def to_params
{
:id => id,
:client_id => client.id,
:serialized_products_and_services => serialized_products_and_services,
:appointment => serializable_hash.each_with_object({}) { |(key, value), hash| hash[key.to_sym] = value }
}
end
def services_with_info
appointment_services.collect { |item|
item.serializable_hash.merge(
"price" => number_with_precision(item.price, :precision => 2),
"label" => item.service.name,
"item_id" => item.service.id,
"type" => "service"
)
}
end
def products_with_info
appointment_products.collect { |item|
item.serializable_hash.merge(
"price" => number_with_precision(item.price, :precision => 2),
"label" => item.product.name,
"item_id" => item.product.id,
"type" => "product"
)
}
end
def payments_with_info
payments.collect { |payment| payment.serializable_hash.merge("method" => payment.payment_method.label) }
end
def in_series(direction)
less_or_greater = (direction == :past) ? "<" : ">"
conditions = [
"start_time #{less_or_greater} :start_time", # with a start time earlier or later than this appointment
"id != :id", # exclude this appointment itself
"recurrence_rule_hash = :hash", # with a hash matching this appointment's hash
"recurrence_rule_hash != ''", # maybe rethink this last condition
].join(" and ")
Appointment.where(conditions, {:start_time => self.start_time.to_s, :hash => self.recurrence_rule_hash, :id => self.id})
end
def future_in_series
in_series(:future)
end
def next_in_series
future_in_series.first
end
def has_future?
future_in_series.count > 0
end
def destroy_future
self.future_in_series.each do |a|
a.destroy
end
end
def update_future
destroy_future
create_future
end
def save_future
drop_out_of_recurrence_if_needed
if repeats? and should_save_future
update_future
end
end
def create_future
if new_record?
raise "Can't create_future on unsaved appointment"
end
(1..49).each do |i|
new = self.dup
new.start_time = self.start_time + (i * self.repeats_every_how_many_weeks).weeks
new.client = self.client
new.should_save_future = false
new.save!
new.reload.copy_services_from(self)
end
end
def copy_services_from(appointment)
appointment.appointment_services.each do |original_appointment_service|
original_appointment_service.dup.update_attributes!(:appointment_id => self.id)
end
end
def drop_out_of_recurrence_if_needed
# If this appointment has been instructed only to update itself, un-repeat.
if only_update_self
assign_attributes(
:can_repeat => false,
:repeats_every_how_many_weeks => 0,
:recurrence_rule_hash => ""
)
end
end
def generate_recurrence_hash_if_needed
if repeats? and recurrence_rule_hash == ""
assign_attributes(:recurrence_rule_hash => (0...50).map{ ('a'..'z').to_a[rand(26)] }.join)
end
end
def unrepeat_if_needed
# If this appointment doesn't repeat but it DOES have a hash, that means
# it used to repeat but no longer does. Delete the rest in the series and un-repeat.
if !repeats? and recurrence_rule_hash != ""
destroy_future
assign_attributes(:recurrence_rule_hash => "")
end
end
def set_time(time, ymd)
self.start_time_time = time
self.start_time_ymd = ymd
self.start_time = Appointment.smush_times(time, ymd)
end
def self.smush_times(time, ymd)
Time.zone.parse(Date.strptime(ymd, "%m/%d/%Y").strftime("%Y/%m/%d") + " " + time)
end
def repeats?
self.repeats_every_how_many_weeks != 0
end
def start_time_time
if @start_time_time.class.name == "NilClass"
if self.start_time != nil
self.start_time.strftime("%I:%M%p")
else
nil
end
else
@start_time_time
end
end
def start_time_time=(t)
@start_time_time = t
end
def start_time_ymd
if (@start_time_ymd.class.name == "NilClass")
if self.start_time != nil
# Converting to an integer then back to a string takes out any leading zeroes or spaces.
self.start_time.strftime("%m").to_i.to_s + "/" + self.start_time.strftime("%e").to_i.to_s + "/" + self.start_time.strftime("%Y");
else
nil
end
else
@start_time_ymd
end
end
def start_time_ymd=(t)
@start_time_ymd = t
end
def end_time
self.start_time + self.calculated_length.minutes
end
def minutes_since_midnight
self.start_time ? (self.start_time.strftime("%H").to_i * 60) + self.start_time.strftime("%M").to_i : nil
end
def total_charge
transaction_item_total("Service") +
transaction_item_total("Product") * (salon.tax_rate.to_f + 1) + tip
end
def paid_for?
payment_total > total_charge
end
def payment_total
payments.sum("amount")
end
def calculated_length
return nil unless self.id
time_block_type.code == "GENERAL_TIME_BLOCK" ? length : appointment_services.map(&:length).sum.to_i
end
def generous_calculated_length
(calculated_length || 0) > 0 ? calculated_length : 15
end
def has_payments
payments.count > 0
end
def checked_out?
has_payments
end
def transaction_items_of_type(type)
TransactionItem.find_by_sql ["
SELECT ti.*
FROM transaction_item ti
JOIN transaction_item_type tit ON ti.transaction_item_type_id = tit.id
JOIN appointment a ON ti.appointment_id = a.id
WHERE tit.label = ?
AND ti.appointment_id = ?
", type, self.id]
end
def transaction_item_label(type)
items = self.transaction_items_of_type(type)
if items.count == 1
# Include item label only
items.map { |ti| ti.label }.join(", ")
else
# Include item label and item price
items.map { |ti| "#{ti.label} ($#{number_with_precision(ti.price, :precision => 2)})" }.join(", ")
end
end
def transaction_item_total(type)
self.transaction_items_of_type(type).map { |ti| ti.price }.sum
end
def service_list
self.services.collect { |s| s.name }.join(", ")
end
def product_list
self.products.collect { |p| p.name }.join(", ")
end
def make_sure_length_makes_sense
if self.time_block_type.code == "APPOINTMENT"
self.length = 0
end
end
def time_block_type_code
time_block_type ? time_block_type.code : ""
end
def day_of_week_index
self.start_time ? self.start_time.wday : nil
end
def client_name_that_fits
self.client.name ? self.client.name.split(" ").map { |w| w[0..0] + "." }.join : nil
end
def stylist_name_index
stylist.cached_order_index
end
def conflicting_with_another_appointment?
wheres = "start_time = ? and is_cancelled = false and client.salon_id = stylist.salon_id"
if new_record?
stylist.appointments.joins(:stylist).joins(:client).where(
wheres,
start_time
).count > 0
else
stylist.appointments.joins(:stylist).joins(:client).where(
"#{wheres} and appointment.id != ?",
start_time,
id
).count > 0
end
end
def free_of_time_conflicts?
!conflicting_with_another_appointment?
end
def self.in_date_range(start_date, end_date)
where(
"start_time between :start_date and :end_date and is_cancelled = false", {
start_date: start_date.midnight,
end_date: end_date.midnight + 1.day - 1.second,
}
)
end
def add_service(service, options = {})
if service.class != Service
service_name = service
service = self.stylist.salon.services.find_by_name(service_name)
raise "Service \"#{service_name}\" not found" if !service
end
raise "Can't add a different salon's service" unless self.stylist.salon_id == service.salon_id
save! if new_record?
AppointmentService.create!(
:appointment_id => self.id,
:service_id => service.id,
:stylist_id => self.stylist.id,
:length => options[:length] || 0,
:price => options[:price] || service.price
)
end
def add_product(product, options = {})
if product.class != Product
product_name = product
product = self.stylist.salon.products.find_by_name(product_name)
raise "Product \"#{product_name}\" not found" if !product
end
raise "Can't add a different salon's product" unless self.stylist.salon_id == product.salon_id
save if new_record?
AppointmentProduct.create!(
:appointment_id => self.id,
:product_id => product.id,
:stylist_id => stylist.id,
:price => options[:price] || product.retail_price,
:quantity => options[:quantity] || 1
)
end
def total_number_of_products
appointment_products.sum("quantity")
end
# Example string:
# [
# {"id":"55","stylist":"Carla","label":"Men's Haircut","stylist_id":"46","item_id":"55","length":"30","quantity":"","price":"26.00","type":"service"},
# {"id":"56","stylist":"Carla","label":"Women's Haircut","stylist_id":"46","item_id":"56","length":"45","quantity":"","price":"35.00","type":"service"}
# ]
def add_services_and_products_from_json_string(string)
ActiveSupport::JSON.decode(string).each do |item|
item["type"] ||= "service"
if item["type"] == "service"
add_service(
item["name"],
:price => item["price"],
:length => item["length"]
)
elsif item["type"] == "product"
add_product(
item["name"],
:price => item["price"],
:quantity => item["quantity"]
)
end
end
end
def add_payments_from_json_string(string)
ActiveSupport::JSON.decode(string).each do |payment|
add_payment(payment["amount"], payment["method"])
end
end
def set_payments_from_json_string(string)
payments.destroy_all
add_payments_from_json_string(string)
end
def set_services_and_products_from_json_string(string)
appointment_services.destroy_all
appointment_products.destroy_all
add_services_and_products_from_json_string(string)
end
def add_payment(amount, method_label)
save if new_record?
payments.create!(
:payment_method_id => PaymentMethod.find_or_create_by_label(method_label).id,
:amount => amount
) unless !valid?
end
def has_service?(service)
if service.class == Service
return services.reload.member?(service)
else
return services.reload.map(&:name).member?(service)
end
end
def clear_services
appointment_services.destroy_all
end
def send_email_reminder
AppointmentReminderMailer.reminder_email(self).deliver
end
def salon
stylist.salon
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment