Created
November 24, 2013 18:25
-
-
Save jasonswett/7630490 to your computer and use it in GitHub Desktop.
A fat model
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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