How to DRY up Webhook handlers in Rails - ruby-on-rails

I've been developing Stripe Webhook handler to create/update records depending the values.
It's not really hard, if it's a simple like this below;
StripeEvent.configure do |events|
events.subscribe 'charge.succeeded' do |event|
charge = event.data.object
StripeMailer.receipt(charge).deliver
StripeMailer.admin_charge_succeeded(charge).deliver
end
end
However If I need to store the data conditionally, it could be little messier.
In here I extracted the each Webhook handler and defined something like stripe_handlers/blahblah_handler.rb.
class InvoicePaymentFailed
def call(event)
invoice_obj = event.data.object
charge_obj = retrieve_charge_obj_of(invoice_obj)
invoice = Invoice.find_by(stripe_invoice_id: charge_obj[:invoice])
# common execution for subscription
invoice.account.subscription.renew_billing_period(start_at: invoice_obj[:period_start], end_at: invoice_obj[:period_end])
case invoice.state
when 'pending'
invoice.fail!(:processing,
amount_due: invoice[:amount_due],
error: {
code: charge_obj[:failure_code],
message: charge_obj[:failure_message]
})
when 'past_due'
invoice.failed_final_attempt!
end
invoice.next_attempt_at = Utils.unix_time_to_utc(invoice_obj[:next_payment_attempt].to_i)
invoice.attempt_count = invoice_obj[:attempt_count].to_i
invoice.save
end
private
def retrieve_charge_obj_of(invoice)
charge_obj = Stripe::Charge.retrieve(id: invoice.charge)
return charge_obj
rescue Stripe::InvalidRequestError, Stripe::AuthenticationError, Stripe::APIConnectionError, Stripe::StripeError => e
logger.error e
logger.error e.backtrace.join("\n")
end
end
end
I just wonder how I can DRY up this Webhook handler.
Is there some best practice to approach this or any ideas?

I suggest re-raising the exception in retrieve_charge_obj_of, since you'll just get a nil reference exception later on, which is misleading. (As is, you might as well let the exception bubble up, and let a dedicated error handling system rescue, log, and return a meaningful 500 error.)
a. If you don't want to return a 500, then you have a bug b/c retrieve_charge_obj_of will return nil after the exception is rescued. And if charge_obj is nil, then this service will raise a NPE, resulting in a 500.
if invoice_obj[:next_payment_attempt] can be !present? (blank?), then what is Utils.unix_time_to_utc(invoice_obj[:next_payment_attempt].to_i) supposed to mean?
a. If it was nil, false, or '', #to_i returns 0 -- is that intended? ([]/{} is also blank? but would raise)
Conceptually, this handler needs to issue a state transition on an Invoice, so a chunk of this logic can go in the model instead:
class Invoice < ApplicationRecord
# this method is "internal" to your application, so incoming params should be already "clean"
def mark_payment_failed!(err_code, err_msg, attempt_count, next_payment_at)
transaction do # payment processing usually needs to be transactional
case self.state
when 'pending'
err = { code: err_code, message: err_msg }
self.fail!(:processing, amount_due: self.amount_due, error: err)
when 'past_due'
self.failed_final_attempt!
else
ex_msg = "some useful data #{state} #{err_code}"
raise InvalidStateTransition, ex_msg
end
self.next_attempt_at = next_payment_at
self.attempt_count = attempt_count
self.save
end
end
class InvalidStateTransition < StandardError; end
end
Note: I recommend a formal state machine implementation (e.g. state_machine) before states & transitions get out of hand.
Data extraction, validation, and conversion should happen in the handler (that's what "handlers" are for), and they should happen before flowing deeper in your application. Errors are best caught early and execution stopped early, before any action has been taken.
There are still some other edge cases that I see that aren't really handled.

Related

Handle exception in ruby on rails

I called a method #txt.watch inside model from worker and Inside watch() there is an array of parameters(parameters = self.parameters). Each parameter have unique reference id.
I want to rescue each exception error for each parameter from inside worker.
class TextWorker
def perform(id)
#txt = WriteTxt.find(id)
begin
#txt.watch
total_complete_watch = if #txt.job_type == 'movie'
#txt.total_count
else
#txt.tracks.where(status:'complete').size
end
#txt.completed = total_completed_games
#txt.complete = (total_complete_games == #txt.total_count)
#txt.completed_at = Time.zone.now if #txt.complete
#txt.zipper if #txt.complete
#txt.save
FileUtils.rm_rf #txt.base_dir if #txt.complete
rescue StandardError => e
#How to find errors for each reference_id here
raise e
end
end
end
Is there any way to do. Thanks u very much.
I assume self.parameters are in your Model class instance. In that case, do as follows and you can reference them.
begin
#txt.watch
rescue StandardError
p #parameters # => self.parameters in the Model context
raise
end
Note:
As a rule of thumb, it is recommended to limit the scope of rescue as narrow as possible. Do not include statements which should not raise Exceptions in your main clause (such as, #txt.save and FileUtils.rm_rf in your case). Also, it is far better to limit the class of an exception; for example, rescue Encoding::CompatibilityError instead of EncodingError, or EncodingError instaed of StandardError, and so on. Or, an even better way is to define your own Exception class and raise it deliberately.

How to handle multiple exceptions in a Transaction with Ruby on Rails for importing a text file

My ruby environment is: Ruby 2.3.1 and Rails 5.0.0.1.
I'm trying to import a text file for import a lot of purchases items.
Example of purchase file:
data.txt
Customer\tDescription\tUnit Price\tQuantity\tAddress\tSupply company\n
Athos Matthew\tChocolate\t10\t5\tSome address.\tChocolate company\n
The columns are divided by a tab (\t) and it has an enter at the final (\n).
I have the purchase class where all attributes cannot be null. The attributes are:
custumer_name:string
product_id:integer # It has relationship with the Product Resource
product_quantity:integer
supply_company_id:integer # It has relationship with the SupplyCompany Resource
To import the file, I decided to create a PurchaseImporter class to do this job and keep the code cleaner.
My problem is that transaction part:
begin
ActiveRecord::Base.transaction do
purchase = Purchase.new
data = line.force_encoding('UTF-8').split(/\t/)
purchase.customer_name = data[0]
product = Product.find_or_create_by!(description: data[1], price: data[2])
purchase.product_quantity = data[3]
purchase.product = product
supply_company = SupplyCompany.find_or_create_by!(name: data[5], address: data[4])
purchase.supply_company = supply_company
purchase.save!
end
rescue Exception => e
#errors[:import][index] = e.message
end
My problem is that I want to catch all the raised errors from the Product, SupplyCompany and Purchase that could happen inside this transaction.
This is the order of the happenings without the unnecessary code to explain it.
product = Product.find_or_create_by!(description: data[1], price: data[2])
supply_company = SupplyCompany.find_or_create_by!(name: data[5], address: data[4])
purchase.save!
I need to print this errors information to this 3 classes in the screen, but with my code, I can only catch the first exception error, generated by the product. If some error happened in the SupplyCompany or in the Purchase, I lost these errors messages.
Are there other ways to import and log the errors message when importing files?
You can have more specific exception handling... do a rescue for each section you want to trap, at the end raise an error if any previous error was encountered (to get you out of the transaction block) and test in that final error that you're rescuing your own raise otherwise it's some other problem and you need to halt.
begin
ActiveRecord::Base.transaction do
error_encountered = false
purchase = Purchase.new
data = line.force_encoding('UTF-8').split(/\t/)
purchase.customer_name = data[0]
begin
product = Product.find_or_create_by!(description: data[1], price: data[2])
purchase.product_quantity = data[3]
purchase.product = product
rescue Exception => e
#errors[:import][index] = e.message
error_encountered = true
end
begin
supply_company = SupplyCompany.find_or_create_by!(name: data[5], address: data[4])
purchase.supply_company = supply_company
rescue Exception => e
#errors[:import][index] = e.message
error_encountered = true
end
begin
purchase.save!
rescue Exception => e
#errors[:import][index] = e.message
error_encountered = true
end
raise 'break out of transaction' if error_encountered
end
rescue Exception => e
raise unless e.message == 'break out of transaction'
end
Since you're rescuing Exception it's hard to know what error is actually emerging. When rescuing, you should try to use a more specific class when possible.
You also might not need to use rescue at all. The active methods you're using: find_or_create_by! and save! can be written without the exclamation point so that they don't raise errors.
In active record, if you try and save something with validation errors then the <record>.errors.full_messages array is populated. It won't necessarily raise an error if you don't use the exclamation point (although errors can be raised from all sorts of things regardless).
So, for example you can try and save a record and check for errors like this:
product = Product.find_or_initialize_by(description: data[1], price: data[2])
product.save
errors[:import][index] ||= []
errors[:import][index].concat product.errors_full_messages
Actually, in this case I think your approach makes some sense. You're saving a few records in sequence. If the first fails, then probably the others will fail - so is it worth even attempting to save those subsequent records? I'll let you decide.

RSpec 3 - raising an error on purpose to test it is rescued

Consider a simple method -
def my_method(users)
eligible_users = []
users.each do |u|
# Go to the next user unless they are eligible
next unless is_eligible?(u)
begin
update_user(u)
eligible_users << u
rescue
puts "Error occured"
# Prints some other stuff about error
next
end
end
end
A key feature of this method is that it loops through users but continues to the next user even if a given user throws an error.
If I were writing spec tests for this, I'd love to pass an array of 3 users and purposely have it error out on the first user. I can then check that the 2nd and 3rd were still correctly processed.
How would I go about raising an error on purpose for only one of the result sets?
I was thinking I could stub is_eligible? and return an error for one of the result and true for the remainder -
allow_any_instance_of(MyClass).to receive(:is_eligible?).and_return(
raise StandardError.new,
true,
true
)
As expected, that doesn't work. Any other approaches?
Thanks!
I can't exactly answer the question, but rather than doing a begin resuce thing,you can follow this approach,
Make the update_user return true or false.
Keep an array of users that falied to update.
return an object like
response: { status: "failure", message: "falied to update #{pluralize(failed_users_array.count, 'user')}", failures: failed_users_array.join(", ") } for failures and
response: { status: "success", message: "#{pluralize(users.count, 'user')} updated successfully" } for all success.
now you can easily test,
have two cases, one where you can test failures and when you can test all success.
Just stub the response object.
For raising errors, you have to do .and_raise("some tezt or StandardError.new") , thats in the docs.

How to avoid a subscription from going out of sync with Stripe?

Given the following method
def change_plan_to(plan_id)
new_plan = Plan.find plan_id
stripe_customer = Stripe::Customer.retrieve(stripe_customer_token)
stripe_customer.update_subscription(plan: new_plan.slug)
self.plan = new_plan
self.active = true
save
rescue Stripe::InvalidRequestError => e
logger.error "[STRIPE] #{ e }"
errors.add :base, "Unable to change your plan!"
false
end
Specifically line #4-6. I want 4 and 5 to happen only if 4 is successful but Stripe doesn't return the ability to wrap that in a if. If it errors it just throws Stripe::InvalidRequestError.
What's the best way to handle this? Fire & forget and allow Stripe webhook callbacks to manage expiring active state as needed?
The other scenario is that all the code will halt after line 4 if it doesn't pass. Is this how rescue works?
Yes thats the way rescue work,
So better you execute these statements which is dependent on line 4 in webhook callbacks that stripe sends to you. Because that ensures subscription change.

Returning true or error message in Ruby

I'm wondering if writing functions like this is considered good or bad form.
def test(x)
if x == 1
return true
else
return "Error: x is not equal to one."
end
end
And then to use it we do something like this:
result = test(1)
if result != true
puts result
end
result = test(2)
if result != true
puts result
end
Which just displays the error message for the second call to test.
I'm considering doing this because in a rails project I'm working on inside my controller code I make calls to a model's instance methods and if something goes wrong I want the model to return the error message to the controller and the controller takes that error message and puts it in the flash and redirects. Kinda like this
def create
#item = Item.new(params[:item])
if !#item.nil?
result = #item.save_image(params[:attachment][:file])
if result != true
flash[:notice] = result
redirect_to(new_item_url) and return
end
#and so on...
That way I'm not constructing the error messages in the controller, merely passing them along, because I really don't want the controller to be concerned with what the save_image method itself does just whether or not it worked.
It makes sense to me, but I'm curious as to whether or not this is considered a good or bad way of writing methods. Keep in mind I'm asking this in the most general sense pertaining mostly to ruby, it just happens that I'm doing this in a rails project, the actual logic of the controller really isn't my concern.
I would say that methods that return different types (e.g. boolean vs. string vs. numbers) under different circumstances are a bad practice.
If you have some sort of test method that wants to return details of why the test has not passed then you can return a pair of values (an Array) as follows:
def test(x)
if x == 1
return true, "x is fine"
else
return false, "Error: x is not equal to one."
end
end
and then write the section of your controller code as:
valid, message = #item.save_image(params[:attachment][:file])
if !valid
flash[:notice] = message
redirect_to(new_item_url) and return
end
If you're talking about a save_image method that will succeed the majority of the time but may fail and you want to indicate this failure and the reason then I would use exceptions e.g.
def save_image(file)
raise "No file was specified for saving" if file.nil?
# carry on trying to save image
end
and then your controller code would be along the lines of:
begin
result = #item.save_image(params[:attachment][:file])
rescue Exception => ex
flash[:notice] = ex.message
redirect_to(new_item_url) and return
end

Resources