Skip to content

Instantly share code, notes, and snippets.

@panSarin
Created March 22, 2014 13:46
Show Gist options
  • Select an option

  • Save panSarin/9707394 to your computer and use it in GitHub Desktop.

Select an option

Save panSarin/9707394 to your computer and use it in GitHub Desktop.
My first services + form objects
class FormObject
include ActiveModel::Model
def initialize(args={})
args.each do |k,v|
instance_variable_set("@#{k}", v) unless v.nil?
end
end
def save
return false unless valid?
save_objects
end
def persisted?
@id.present?
end
end
#encoding: utf-8
class PoliciesController < AuthenticationController
def new
@policy_form = PolicyForm.new(insurance_started_at: Time.now, insurance_finished_at: Time.now+60*60*24*365 - 1.day)
end
def new_from_import_record
@import_record = params[:import_record_class].constantize.find(params[:import_record_id])
policy_params = @import_record.policy_params(params[:is_renewal].present?, params[:renewal_policy_present].present?)
@policy_form = PolicyForm.new(policy_params)
render :new
end
def create
result = policy_service.process_create
@policy_form = result[:policy_form]
set_flash_message(result)
if @policy_form.errors.messages.blank?
redirect_to policies_path
else
render :new
end
end
def edit
@policy_form = PolicyForm.new(Policy.find(params[:id]).attributes)
end
def update #there are few cases where user should be redirected so that looks messy (refactorable with some method probably?)
result = policy_service.process_update
@policy_form = result[:policy_form]
set_flash_message(result)
if @policy_form.errors.blank?
if params[:redirect_after_save_to].present?
redirect_to params[:redirect_after_save_to]
elsif params[:clearing_view] == 'true'
redirect_to clearing_policy_path(@policy_form.policy.id)
else
redirect_to policy_path(@policy_form.policy.id)
end
else
if params[:policy][:users_attributes].present?
render :edit
else
redirect_to policy_path(@policy_form.policy.id)
end
end
end
private
def policy_service
PolicyService.new(current_user, params, request)
end
end
class PolicyForm < FormObject
POLICY_FIELDS = [:clearing_status, :client_id, :client_type, :insurance_started_at, :insurance_finished_at, :number, :renewal, :renewed_policy_id,
:renewal_number, :payment_type, :program_id, :rejected_from_clearing_notes, :series_id, :status, :producted_at,
:sent_to_nau_at, :sent_to_client_at, :sent_to_clearing_at, :sent_to_insurance_agency_at ]
attr_accessor *POLICY_FIELDS
attr_accessor :id, :product_category_id
attr_accessor :installments, :users, :products, :current_user
attr_accessor :rewrite_additional_data, :client_name, :rewrite_attachments, :fake_field
def initialize(args={})
super
@payment_type ||= 1
set_installments
set_users
set_products
products_amount
end
def policy_attrs
attrs = {}
POLICY_FIELDS.each do |f|
attrs[f] = instance_variable_get("@#{f}") if instance_variable_defined?("@#{f}")
end
attrs
end
validate do
unless policy.valid?
policy.errors.each do |key, values|
errors[key] = values
end
end
end
validate do
if @installments.present?
if payment_type.to_s == '1'
@installments.first.payment_amount = products_amount
else
errors['installments_amount'] = I18n.t('policy.form_errors.installments_amount') if products_amount != installments_amount
end
end
end
validate do
@installments.each do |installment|
installment.valid?
installment.errors.each do |key, values|
errors[key] = values
end
end
end
validate do
if policy.renewal && @current_user.cannot?(:change_recommender, Policy) && is_recommender_changed?
errors['recommender_changed'] = I18n.t('policy.form_errors.recommender_changed')
end
end
def policy
@policy ||= @id.present? ? Policy.find(@id) : Policy.new
@policy
end
def program
@program_id.present? ? Program.find(@program_id) : nil
end
delegate :product_category, to: :program
def create
@policy = Policy.new(policy_attrs)
save
end
def update
@policy = Policy.find(@id)
@policy.attributes = policy_attrs
save
end
def save_objects
ActiveRecord::Base.transaction do
begin
set_renewal_client_and_insurance_agency if policy.renewal
policy.save
assign_attachments
if @rewrite_additional_data
assign_users
assign_products
assign_installments
policy.count_commissions
end
true
rescue Exception => e
e
end
end
end
def set_products
@products = []
if @products_attributes.present?
@products_attributes.each do |key, value|
@products << PoliciesProduct.new(product_id: key, amount: value[:amount])
end
elsif policy.persisted?
@products = policy.policies_products
end
end
def set_installments
@installments = []
if @installments_attributes.present?
@installments_attributes.each do |key, installment|
@installments << Installment.new(policy: policy, payment_date: installment[:payment_date], payment_amount: installment[:payment_amount], payment_number: installment[:payment_number] )
end
elsif policy.persisted?
@installments = policy.installments
end
end
def set_users
@users = []
if @users_attributes.present?
@users_attributes.each do |k, pu|
if pu['user_id'].present?
user = User.find(pu['user_id'])
@users << PoliciesUser.new(user_id: user.id , sale_role_id: pu['sale_role_id'], contract_id: user.contract_id)
end
end
elsif policy.persisted?
@users = policy.policies_users
end
end
private
def assign_users
policy.policies_users.destroy_all
@users.each do |pu|
pu.policy = policy
pu.save!
end
end
def assign_products
policy.policies_products.destroy_all
@products.each do |pp|
pp.policy = policy
pp.save!
end
end
def assign_attachments
if @attachments_attributes.present?
@attachments_attributes.each do |key, attachment|
Attachment.create!(asset: attachment['asset'], kind: attachment['kind'], comment: attachment['comment'], record: policy)
end
end
end
def assign_installments
policy.installments.destroy_all
@installments.each do |installment|
installment.policy = policy
installment.save
end
end
def products_amount
if @products_attributes.present?
@products_attributes.values.inject(0){|s,v| s+v[:amount].to_i}
else
0
end
end
def installments_amount
if @installments_attributes.present?
@installments_attributes.values.map{|i| i['payment_amount'].to_i}.inject(0, :+).to_i
else
0
end
end
def is_recommender_changed?
if @users_attributes.present?
policy.renewed_policy.policies_users.find_by_sale_role_id(3).try(:user).try(:id).to_s != @users_attributes['2'][:user_id]
else
false
end
end
def set_renewal_client_and_insurance_agency
if policy.renewed_policy.present?
policy.renewal_client = true if policy.client == policy.renewed_policy.client
policy.renewal_insurance_agency = true if policy.program.insurance_agency == policy.renewed_policy.program.insurance_agency
end
end
end
# it is my first service ever so i would be glad if you have some ideas how should i change it . Even if i have to remove whole code, and refactor all controllers ;]
class PolicyService < BaseService
def audit_params
remove_attachments_attributes(@params, 'policy')
end
def process_create
policy_form = PolicyForm.new(params[:policy].merge(current_user: @current_user, audit_params: audit_params, audit_referer: @request.referer))
result = { }
if policy_form.create
result[:notice] = I18n.t('policy.create_success')
PoliciesMailer.past_insurance_notification(policy_form.policy).deliver if policy_form.policy.insurance_started_at + 60*60*24 < Time.now
end
result[:policy_form] = policy_form
result[:errors] = policy_form.errors
result
end
def process_update
policy_form = PolicyForm.new(params[:policy].merge(current_user: @current_user, id: params[:id], audit_params: audit_params, audit_referer: @request.referer))
result = { }
result[:notice] = I18n.t('policy.update_success') if policy_form.update
result[:policy_form] = policy_form
result[:errors] = policy_form.errors
result
end
end
@andrzejkrzywda

Copy link
Copy Markdown

The controller looks mostly ok.

You went from a fat controller to a fat form. I wonder how the form UI actually look like. Can you show a screenshot? The form object is definitely too big, it's responsibilities should be just collecting params and run very simple validations (presence, size), not much more. All else goes to the service and the domain objects, in theory.

@panSarin

Copy link
Copy Markdown
Author

https://imageshack.com/i/jwjngkp
https://imageshack.com/i/eu3r1wp
Those are UIs.
So you are saying that all stuff with initializing association data in proper format should be done in service instead of form object? So all my "set_#{products/installments/etc}" should be in service?
The products_amount and installments_amount is for displayin amount of given record in the form - so i think those 2 can stay here ?

Also i thinking about extracting PolicyForm to NewPolicyForm and EditPolicyForm so i won't have so many ifs in almost each method.

@ml

ml commented Mar 24, 2014

Copy link
Copy Markdown

Don't check for errors like this if @policy_form.errors.messages.blank? , just redirect to policies_path, raise an exception from the form object and add rescue_from FormObjectExceptionClass { |_| render :new }

@panSarin

Copy link
Copy Markdown
Author

About those errors: but if first error will raise exception i will not have @form.errors array to display all validation errors / highlight required inputs f.e.

@madsheep

Copy link
Copy Markdown

A good starting point for refactoring might be all those methods that mention different objects (users products attachments) - those are different objects, so how about making them work within smaller form objects that you only instantiate here?

@panSarin

Copy link
Copy Markdown
Author

So i in service i should initalize my policy object and all associations object. Then give those associated objects to policy form, and render it ? That way i get smaller policy form but more "form objects" and i can use them when other resource will have f.e. products assocaition also?

@fidel

fidel commented Mar 26, 2014

Copy link
Copy Markdown

I think that #save is not a responsibility of a form object. It's the service responsibility. Form object should run only trivial validations and pass necessary data to the service. I really appreciate Adam Hawkins approach to form objects: http://hawkins.io/2014/01/form_objects_with_virtus/
Using rescue_from in controller is not a good idea imho. Why leak such things to controller layer?

@ml

ml commented Mar 27, 2014

Copy link
Copy Markdown

Adan Hawkins does exactly what you think is not good idea - raises a ValidationError from the form object. I think that having a rescue_from in the controller to handle validation errors from create, update and other actions touching the record in an uniform way is better than checking @obj.errors.any? in every single action. I don't understand "leak such things to controller layer" - what things? Exceptions from the object that the controller collaborates with directly? "Tell, don't ask".

@ml

ml commented Mar 27, 2014

Copy link
Copy Markdown

BTW is there any significant difference between new and edit views? Couldn't it be unified?

@ml

ml commented Mar 27, 2014

Copy link
Copy Markdown

I agree that form object should only validate basic things regarding input, more complex logic validations belong to the service. Though, I don't agree that FormObject should pass anything to the service. I think that the FormObject should be injected to the service as a data provider, not created in the constructor, more less like this:

class Serivce
  attr_acessor :data_source
  def initalize(data_source)
    self.data_source = data_source
  end

  def update
    data_source.validate! and validate! and process
  end

  private 

  def validate!
    # logic valdiation
  end

  def process
    # do stuff
  end
end

# controller
  def update
     form_object = PolicyForm.new(params[:policy])
     PolicyService.new(form_object).update
  end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment