Allowing only one update for specific param? - ruby-on-rails

How can I make it so once a param is filled, it can't be updated or edited again?
Issue: I have a param, :video, that once filled it will charge the customer. I want to make it so once it's filled, it can't be updated or edited again.
I have taken preventative measures on the views side that once a video is uploaded the edit/update form is hidden and can't be created again but one thing i noticed that if an update fails for any reason, the page can be went back to and the update form can be filled out again (even if the video is uploaded), thus again charging the customer twice.
How it works:
A customer creates an order, a stripe id is created, once the seller fulfills the order (by uploading a file or video), the charge is captured.
def charge_update
respond_to do |format|
#amount = (#order.order_price).to_i * 100
#amount_seller = (#order.order_price).to_i * 75
if #order.update(order_charge)
begin
charge = Stripe::Charge.create({
:amount => (#order.order_price).to_i * 100,
:description => 'Rails Stripe customer',
:currency => 'usd',
:customer => #order.stripe_customer_token,
:destination => {
:amount => #amount_seller ,
:account => (#order.seller.stripe_token),
}
})
rescue Stripe::CardError => e
charge_error = e.message
end
#order.update_column(:order_status, 2)
format.html { redirect_to ([#user, #order]), notice: 'Order was successfully uploaded.' }
format.json { render :show, status: :ok, location: #order }
elsif
if charge_error
flash[:error] = charge_error
redirect_to user_order_path([#user, #order])
else
format.html { render :edit }
format.json { render json: #order.errors, status: :unprocessable_entity }
end
end
end
end
private
def order_charge
params.require(:order).permit(:video, :order_status)
end
Feel free to let me know if there is a better way to go about validating the charge on the customer. My biggest concern is a customer being charged more than once because a seller either messes up, is malicious, etc. I want to guarantee a customer will only be charged once. The only way I can think of is to only allow one update to the video column.
Either that or creating a separate table for videos with order_id being unique to each video table ID.
UPDATE
I ended up doing the following which works... If any has any ideas of a better way, I am open to suggestions!
def charge_update
respond_to do |format|
if #order.video.present?
format.html { redirect_to ([#user, #order]), notice: 'Order already completed!.' }
format.json { render :show, status: :ok, location: #order }
else
#amount = (#order.order_price).to_i * 100
#amount_seller = (#order.order_price).to_i * 75
if #order.update(order_charge)
begin
...
...
...
end

The better way is to make a table that has these values
order_id
video_id
user_id
charged (Boolean Value )
once the customer made an order and charged it , you can fill these values and make charged value as true .
you can check if he made an order for that video before or not before making a new payment for it again .
here is an example for the model you can make for that
class CustomerOrder < ActiveRecord::Base
belongs_to :customer
belongs_to :order
belongs_to :video
end
before the customer make a new order , you can check if there is any relation
#customerorder =CustomerOrder.where(video_id: #video.id , user_id: #user.id , charged :true )

I would implement a callback with ActiveModel::Dirty to initiate a rollback on any change after changing the column value to anything not from empty or nil.
class Amount < ActiveRecord::Base
include ActiveModel::Dirty
before_save :check_video
attr_accessor :video # or whatever your video attribute is
private
def check_video
false if video_changed? && !video_was.nil? # or empty or whatever
true
end
end

I would ensure in the model layer that a record cannot be updated anymore once it was successfully created.
One simple way might be to override the readonly? method that is used by Rails to check for read-only records internally:
# in your model
def readonly?
super || video.present? && persisted?
end
Calling save on a read-only instance will raise an ActiveRecord::ReadOnlyRecord exception.
With this method, you can write if #order.readonly? in your controller and view to check if the #order is still updatedable.

I ended up creating a controller only for uploading the video files.
This way I can create the upload with the charges code in the create, and then allow updating without the need to separate the updates...
This is the gist of the controller so far:
def create
#video_order = VideoOrder.new(video_order_params)
#order = Order.find(params[:order_id])
#video_order.order_id = #order.id
respond_to do |format|
if #order.video_order.present?
format.html { redirect_to #order, notice: 'Already complete dog!.' }
format.json { render :show, status: :created, location: #video_order }
else
if #video_order.valid?
begin
charge = Stripe::Charge.create({
:amount => (#order.order_price).to_i * 100,
:description => 'Rails Stripe customer',
:currency => 'usd',
:customer => #order.stripe_customer_token,
:destination => {
:amount => #amount_seller ,
:account => (#order.seller.stripe_token),
}
})
rescue Stripe::CardError => e
charge_error = e.message
end
if charge_error
flash[:error] = charge_error
redirect_to '/'
else
if #video_order.save
#order.update_column(:order_status, 2)
format.html { redirect_to #order, notice: 'Video order was successfully created.' }
format.json { render :show, status: :created, location: #video_order }
else
format.html { render :new }
format.json { render json: #video_order.errors, status: :unprocessable_entity }
end
end
end
end
end
end
# PATCH/PUT /video_orders/1
# PATCH/PUT /video_orders/1.json
def update
respond_to do |format|
if #video_order.update(video_order_params)
format.html { redirect_to user_order_path([#user, #order]), notice: 'Video order was successfully updated.' }
format.json { render :show, status: :ok, location: #video_order }
else
format.html { render :edit }
format.json { render json: #video_order.errors, status: :unprocessable_entity }
end
end
end
I also have preventative measures in the create method just in case.

Related

Should I be creating multiple controllers or multiple update methods in one controller?

Issue: I have a controller where I want to separate the update action into 3 separate update actions... Update for all params (minus the other update actions), update for changing order status (created, cancelled, charged), and an update for charging (charges only occur once a file is uploaded to the order)
Question: Can you help direct and guide me in which route i should be going in to accomplishing this?
How should i go about doing this in the most efficient and "right" way?
Should i create 2 more controllers (since i need 3 updates), one for changing order status, and one for charging and have them inherit the OrderController?
Or is there a way to have 3 update actions in one controller? I tried this, but couldn't figure it out
Here is how I "solved" it, but it doesn't seem to run too smoothly as sometimes i notice when i upload a file to the order, it starts looping and/or takes longer than just having it all under one defined param method:
def update
respond_to do |format|
if #order.update(order_status)
if user_signed_in?
format.html { redirect_to ([#user, #order]), notice: 'Order was successfully order_status.' }
format.json { render :show, status: :ok, location: #order }
else
format.html { render :edit }
format.json { render json: #order.errors, status: :unprocessable_entity }
end
end
if #order.update(order_charge)
#amount = (#order.order_price).to_i * 100
#amount_seller = (#order.order_price).to_i * 75
if #order.update(order_charge)
if current_user.seller?
charge = Stripe::Charge.create({
:amount => #amount,
:description => 'Rails Stripe customer',
:currency => 'usd',
:customer => #order.stripe_customer_token,
:destination => {
:amount => #amount_seller ,
:account => (#order.seller.stripe_token),
}
})
#order.order_status = "charged"
format.html { redirect_to ([#user, #order]), notice: 'Order was successfully uploaded.' }
format.json { render :show, status: :ok, location: #order }
else
format.html { render :edit }
format.json { render json: #order.errors, status: :unprocessable_entity }
end
end
end
if #order.update(order_params)
if user_signed_in?
format.html { redirect_to ([#user, #order]), notice: 'Order was successfully updated.' }
format.json { render :show, status: :ok, location: #order }
else
format.html { render :edit }
format.json { render json: #order.errors, status: :unprocessable_entity }
end
if buyer_signed_in?
format.html { redirect_to ([#user, #order]), notice: 'Order was successfully updated.' }
format.json { render :show, status: :ok, location: #order }
else
format.html { render :edit }
format.json { render json: #order.errors, status: :unprocessable_entity }
end
end
end
end
private
def set_order
#order = Order.find(params[:id])
end
def order_params
params.require(:order).permit(:name, :email, :image, :video, :description)
end
def order_status
params.require(:order).permit(:order_status)
end
def order_charge
params.require(:order).permit(:video)
end
...
end
This technically works, but not exactly fast or how i want it to. When i upload a video, it gives me the order_status message, which is because i change the order status when a video gets uploaded. But it still works in the sense that a charge is created.
Has anyone ever made an app where they needed multiple update actions under one controller and model and found the best way to go about doing it?
Even though the above code "works", i was told it was not efficient and I should create 2 more controllers to go about what i want to do. I figured I would come here before i divulge in doing that to get other peoples opinions based on their experience if they have had similar apps with this issue.
Or maybe aside from the above 1 and 2, you have more ideas?
(The update action above isn't complete, such as the order_status. But I'm mostly concerned on how I should actually go about doing this.)
One way to approach this would be to just have one order_params method and remove order_charge and order_status because whatever the case only one resource is being updated. And as you said the user is charged only in one case when file is uploaded, so instead have a helper method in the controller that would be responsible to charge the user.
def charge_amount
#amount = (#order.order_price).to_i * 100
#amount_seller = (#order.order_price).to_i * 75
if current_user.seller?
charge = Stripe::Charge.create({
:amount => #amount,
:description => 'Rails Stripe customer',
:currency => 'usd',
:customer => #order.stripe_customer_token,
:destination => {
:amount => #amount_seller ,
:account => (#order.seller.stripe_token),
}
})
#order.order_status = "charged"
end
end
And inside your update method just update the order and charge the user if required. It would be something like,
def update
respond_to do |format|
if user_signed_in? #this could be moved to a `before_action` that authenticates the user
if #order.update(order_status)
if order_params[:video].present?
charge = charge_amount
end
format.html { redirect_to ([#user, #order]), notice: 'Order was successfully order_status.' }
format.json { render :show, status: :ok, location: #order }
else
format.html { render :edit }
format.json { render json: #order.errors, status: :unprocessable_entity }
end
end
end
end
With this approach the code would shrink in size and separates the logic and would be easier to debug. I hope this answers your question or atleast provide you some guidance.
update
If you want separate actions, just create three methods in your controller and depending upon the type of params call the appropriate method in your update action.
I ended up doing this... thanks to a SO post i found earlier...
Orders controller: (separate from update action:)
def charge_update
respond_to do |format|
#amount = (#order.order_price).to_i * 100
#amount_seller = (#order.order_price).to_i * 75
if #order.update(order_charge)
charge = Stripe::Charge.create({
:amount => (#order.order_price).to_i * 100,
:description => 'Rails Stripe customer',
:currency => 'usd',
:customer => #order.stripe_customer_token,
:destination => {
:amount => #amount_seller ,
:account => (#order.seller.stripe_token),
}
})
#order.update_column(:order_status, 2)
format.html { redirect_to ([#user, #order]), notice: 'Order was successfully uploaded.' }
format.json { render :show, status: :ok, location: #order }
else
format.html { render :edit }
format.json { render json: #order.errors, status: :unprocessable_entity }
end
end
end
routes:
resources :orders do
member do
patch :charge_update
put :charge_update
end
end
And for the upload in the view:
<%= form_for([#listing, #order], :url => charge_update_order_path ) do |form| %>
...
<%= form.file_field :video %>
...
<%= form.submit "Upload", class: "btn btn-success" %>
rest of controller: (private section)
private
def order_charge
params.require(:order).permit(:video, :order_status)
end
Now the uploading video action is separate and works faster. I haven't done the same for updating the order status to cancel, which i may or may not after doing more testing. The charging aspect was the most important thing to separate from the rest of the params.

rails redirect in controller for a loop

I have create method like this in rails controller:
def create
#sale = Sale.new(sale_params)
#sale.user_id = current_user.id
#sale.total = #sale.total_all
params[:sale][:items_attributes].values.each do |p|
#product = Product.find(p['product_id'])
if #product.quantity.to_i - p['quantity'].to_i <= 0
flash.now[:notice] = "Quantity is higher then stock"
end
end
respond_to do |format|
if #sale.save
format.html { redirect_to #sale, notice: 'Sale was successfully created.' }
format.json { render :show, status: :created, location: #sale }
else
format.html { render :new }
format.json { render json: #sale.errors, status: :unprocessable_entity }
end
end
end
if #product.quantity.to_i - p['quantity'].to_i <= 0
flash.now[:notice] = "Quantity is higher then stock"
end
this condition fails it means the controller redirected to create page itself otherwise it needs to redirect to index page. If I puts render :new in loop rails says error. How to do this?
You can move it to the model as Harsh suggested. You'd do it something like this. Then you can get rid of the loop.
class Sale < ActiveRecord::Base
has_many :items
# Also make sure you validate children
validates_associated :items
validate :items_in_stock
def items_in_stock
items.each do |i|
if i.quantity > i.product.quantity
i.errors.add(:quantity) = "is not currently available"
end
end
end
end

Rails 4 - How to set db attribute on create

This seems like a dumb question, but how do I update a database field from a model method? The incident.incident_number value is displayed on all the forms and emails, but is NULL in the database:
incident.rb
validate :incident_number, :on => :save
def incident_number
(self.created_at.strftime("%Y") + self.created_at.to_date.jd.to_s + self.id.to_s).to_i
end
incidents_controller.rb
def create
#incident = Incident.new(incident_params)
#incident.user_id = current_user.id
#incident.state_id = 1
respond_to do |format|
if #incident.save
IncidentMailer.new_incident_notification(#incident).deliver
format.html { redirect_to my_incidents_path, notice: 'Incident was successfully created.' }
format.json { render action: 'show', status: :created, location: #incident }
else
format.html { render action: 'new' }
format.json { render json: #incident.errors, status: :unprocessable_entity }
end
end
end
Your current method doesn't persist to the database, it just runs the logic every time the method is called.
There are a number of ways to do this - either via write_attributes (works well in before_save)
before_save :update_incident_number, if: Proc.new { conditional_logic_here }
...
def update_incident_number
write_attribute(:incident_number, new_value)
end
Or, use update_attribute(s)
after_commit :update_incident_number, if: Proc.new { conditional_logic }
...
def update_incident_number
self.update_attribute(:incident_number, new_value)
end
There are a few ways to skin this cat, try a few and see which you prefer based on what callbacks are triggered, and how they deal with your record / changed properties.

Rails - How to accept an array of JSON objects

How do I accept an array of JSON objects on my rails site? I post something like
{'team':{'name':'Titans'}}
However, if I try to post a JSON with an array of objects. It only saves the 1st object.
{'team':[{'name':'Titans'},{'name':'Dragons'},{'name':'Falcons'}]}
My goal is to send multiple 'teams' in 1 JSON file. What do I have to write on the Rails side?
On the rails side, I have something like
def create
#team = Team.new(params[:team])
#team.user_id = current_user.id
respond_to do |format|
if #team.save
format.html { redirect_to(#team, :notice => 'Team was successfully created.') }
format.json { render :json => #team, :status => :created, :location => #team }
else
format.html { render :action => "new" }
format.json { render :json => #team.errors, :status => :unprocessable_entity }
end
end
end
Do I take the params: and for each element, create a new team or something? I'm new to ruby so any help would be appreciated.
Let me assume you post
{'team':[{'name':'Titans'},{'name':'Dragons'},{'name':'Falcons'}]}
Then your params will be
"team" => {"0"=>{"chapter_name"=>"Titans"}, "1"=>{"chapter_name"=>"Dragons"}, "2"=>{"chapter_name"=>"Falcons"}}
My idea is
def create
#insert user id in all team
params[:team].each_value { |team_attributes| team_attributes.store("user_id",current_user.id) }
#create instance for all team
teams = params[:team].collect {|key,team_attributes| Team.new(team_attributes) }
all_team_valid = true
teams.each_with_index do |team,index|
unless team.valid?
all_team_valid = false
invalid_team = teams[index]
end
end
if all_team_valid
#teams = []
teams.each do |team|
team.save
#teams << team
end
format.html { redirect_to(#teams, :notice => 'Teams was successfully created.') }
format.json { render :json => #teams, :status => :created, :location => #teams }
else
format.html { render :action => "new" }
format.json { render :json => invalid_team.errors, :status => :unprocessable_entity }
end
end

Rails 3: Custom Model Notices

Currently I verify that there are no duplicated Members when trying to create a new Member and add it to a Team.
members_controller.rb
def create
#team = current_team
player = Player.find(params[:player_id])
#member = #team.add_player(player.id)
respond_to do |format|
if #member.save
format.html { redirect_to(#team, :notice => 'Member was successfully added.') }
format.js { #current_member = #member }
format.xml { render :xml => #member,
:status => :created, :location => #member }
else
format.html { redirect_to(#team, :notice => 'Member already exists.') }
format.xml { render :xml => #member.errors,
:status => :unprocessable_entity }
end
end
end
team.rb
def add_player(player_id)
current_member = members.build(:player_id => player_id)
current_member
end
I want to add some logic to my add_player method in team.rb that checks various properties of the player that is being added. This action will require multiple failure messages, other than 'Member already exists.' How do I do this in the Model layer?
You can create custom errors on ActiveRecord models. These custom errors can have their own messages, which you can query in your controller if the save is not successful:
# app/models/team.rb
def add_player(player_id)
current_member = members.build(:player_id => player_id)
errors.add(:player_id, 'Custom error message here') if condition
errors.add(:base, 'Custom error message here') if condition
current_member
end
# app/controllers/members_controller.rb
def create
#team = current_team
player = Player.find(params[:player_id])
#member = #team.add_player(player.id)
respond_to do |format|
if #member.save
format.html { redirect_to(#team, :notice => 'Member was successfully added.') }
format.js { #current_member = #member }
format.xml { render :xml => #member,
:status => :created, :location => #member }
else
format.html { redirect_to(#team, :notice => #member.errors.full_messages) }
format.xml { render :xml => #member.errors,
:status => :unprocessable_entity }
end
end
end
More information on custom ActiveRecord validation errors here: http://api.rubyonrails.org/v2.3.8/classes/ActiveRecord/Errors.html#M001725
The controller logic to display all errors from base worked. However, I was not able to add errors from the add_player method as Ben suggested. I instead created separate custom validations as such:
Team.rb
validate validation_name
def validation_name
if condition
errors.add_to_base "Error Message"
end
end

Resources