I use gem stripe_event with stripe webhook.
My initial code is:
config/initializers/stripe.rb
StripeEvent.configure do |events|
events.subscribe 'checkout.session.completed', StripeCheckoutSessionService.new
end
app/services/stripe_checkout_session_service.rb
class StripeCheckoutSessionService
def call(event)
order = Order.find_by(checkout_session_id: event.data.object.id)
order.update(state: "paid")
end
end
My problem is that if order is nil, I have a 500 error, and I want to have a 422 error instead.
I try that :
app/services/stripe_checkout_session_service.rb
class StripeCheckoutSessionService
def call(event)
order = Order.find_by(checkout_session_id: event.data.object.id)
if order
order.update(state: "paid")
head :accepted
else
head :unprocessable_entity
end
end
end
head is not recognize by the service, I try to return a string instead and return head directly in the block but I'm locked now...
Does anybody have any idea for me ?
If you want to return a string, probably you can do this:
class StripeCheckoutSessionService
def call(event)
order = Order.find_by(checkout_session_id: event.data.object.id)
if order
order.update(state: "paid")
"Successfully updated!" # or do this { status: 400, msg: 'success' }
else
"Error occurred!" # or do this { status: 422, msg: 'unprocessable_entity' }
end
end
end
Haven't tried this but should work.
Related
In my application the render is not showing the response on browsers console.
my controller class is-
def create
ActionItem.transaction do
#action = #doc.action_items.new(action_item_params)
#action.minutes_section = #section if #section
# Were we passed a sort_order? If not, default to the highest sort
# order number for this action item's section. minutes-app may
# also pass us -1 to indicate we should compute the next value.
if !action_item_params.key?(:sort_order) or [-1, nil, ""].include?(action_item_params[:sort_order])
result = ActionItem.where(minutes_document_id: #doc.id, minutes_section_id: #section.id).maximum('sort_order')
if result.nil?
#action.sort_order = 0
else
#action.sort_order = result + 1
end
end
if #action.save!
#action.action_items_statuses.create!(status: 'incomplete', status_type: 'completion', contact_id: current_user.id)
#action.action_items_statuses.create!(status: 'unsent', status_type: 'notification', contact_id: current_user.id)
#action.action_items_statuses.reload
handle_assignees(#action, params[:data][:attributes][:assignees]) if !params[:data][:attributes][:assignees].blank?
handle_note(#action, params[:data][:attributes][:note])
render(json: #action, status: 201)
else
render(json: { error: #action.errors }, status: 500)
end
end
rescue ActiveRecord::ActiveRecordError => e
Rails.logger.error e.message
render(json: { error: e.message }, status: 500)
end
I am not getting the response on console-
Error
I've got Rails 5 app with dry-monads on board. Monads are used to create the Appointment object inside create action in AppointmentsController. They return Success or Failure in the last step with below structure:
# services/appointments/create.rb
(...)
def call
Success(appointment_params: appointment_params)
(...)
.bind(method(:save_appointment))
end
private
def save_appointment(appointment)
if appointment.save
Success(appointment)
else
Failure(failure_appointments: appointment, appointments_errors: appointment.errors.full_messages)
end
end
After each action (success or failure) I want to send an email and display the corresponding json in AppointmentsController:
class Api::AppointmentsController < ApplicationController
def create
succeeded_appointments = []
failure_appointments = []
appointments_errors = []
batch_create_appointments_params[:_json].each do |appointment_params|
appointment = ::Appointments::Create.new(appointment_params).call
if appointment.success?
succeeded_appointments << appointment.value!
else
failure_appointments << appointment.failure[:failure_appointments] &&
appointments_errors << appointment.failure[:appointments_errors]
end
end
if failure_appointments.any?
AppointmentMailer.failed_mail(email, failure_appointments.size, appointments_errors).deliver_now
render json: {
error: appointments_errors.join(', '),
}, status: :bad_request
elsif succeeded_appointments.any?
AppointmentMailer.success_mail(email, succeeded_appointments.size).deliver_now
render json: {
success: succeeded_appointments.map do |appointment|
appointment.as_json(include: %i[car customer work_orders])
end,
}
end
end
I wonder if there is a better, faster way to record these errors than declaring 3 different empty arrays (succeeded_appointments, failure_appointments, appointments_errors) like at the beginning of create action? so far the create action looks heavy.
Create a separate service object for bulk creation:
# services/appointments/bulk_create.rb
class Appointments::BulkCreate
def initialize(bulk_params)
#bulk_params = bulk_params
end
def call
if failed_results.any?
AppointmentMailer.failed_mail(email, failed_results_errors.size, failed_results_errors).deliver_now
Failure(failed_results_errors.join(', '))
else
AppointmentMailer.success_mail(email, success_appointments.size).deliver_now
Success(success_appointments)
end
end
private
attr_reader :bulk_params
def failed_results
results.select(&:failure?)
end
def success_results
results.select(&:success?)
end
def success_appointments
#success_appointments ||= success_results.map do |appointment|
appointment.as_json(include: %i[car customer work_orders])
end
end
def failed_results_errors
#failed_results_errors ||= failed_results.map do |failed_result|
failed_result.failure[:appointments_errors]
end
end
def results
#results ||= bulk_params.map do |appointment_params|
::Appointments::Create.new(appointment_params).call
end
end
end
Then your controller will look like this:
class Api::AppointmentsController < ApplicationController
def create
result = ::Appointments::BulkCreate.new(batch_create_appointments_params[:_json]).call
if result.success?
render json: { success: result.value! }, status: :ok
else
render json: { error: result.failure }, status: :bad_request
end
end
end
I am dealing with Shopify order creation webhooks and it is triggering multiple times.
def perform(shop_domain:, webhook:)
shop = Shop.find_by(shopify_domain: shop_domain)
found = false
order_item_ids = []
webhook['line_items'].each do |line_item|
line_item['properties'].each do |property|
if property['name'] == 'BJ_PROD'
found = true
order_item_ids << line_item['product_id']
end
end
end
if found
#order = Order.create({
shop: shop,
shopify_order_id: webhook["id"],
status: 'Ordered',
sent_to_admin: false,
order_details: webhook
})
end
session = ShopifyAPI::Session.new(shop.shopify_domain, shop.shopify_token)
session = ShopifyAPI::Base.activate_session(session)
order_item_ids.each do |order_item_id|
product = ShopifyAPI::Product.find(order_item_id)
product.published_at = nil
product.save!
end
shop.with_shopify_session do
end
render status: 200, json: #order.to_json
end
I am trying to use render status: 200 and head :ok in my OrdersCreateJob class but it is giving error as it is being used in the background job.
Does anybody have an idea of how to deal with such scenarios?
I have an index method in a Rails API controller that is quite horrendous, as you can see below.
I am sure there is a more Ruby or Rails way to write this.
The action supports paging and filtering (by a filter= query parameter) and also can supply a customer-id to restrict what is returned to only proposals relevant to the provided customer.
I wonder if maybe I should separate the customer functionality to a separate endpoint? (eg. customers/:id/proposals). Of course that endpoint would also need to support paging and filter, so I think I might not end up with DRY code. Is there a way (like with Concerns) that I could make this index code simpler (ie. without all the if...then...else)?
def index
if params[:page].present?
page = params[:page]
if params[:filter].present?
if params[:customer_id].present?
#proposals = current_user.retailer.proposals.customer(params[:customer_id]).search(params.slice(:filter)).page(page)
else
#proposals = current_user.retailer.proposals.search(params.slice(:filter)).page(page)
end
else
if params[:customer_id].present?
#proposals = current_user.retailer.proposals.customer(params[:customer_id]).page(page)
else
#proposals = current_user.retailer.proposals.page(page)
end
end
render json: #proposals, root: 'proposals', meta: pagination_dict(#proposals)
else
render status: :bad_request, json: { message: "Please supply page parameter" }
end
end
Here are the Proposal model scopes:
default_scope { order("updated_at DESC") }
scope :filter, -> (term) { where("lower(first_name) || ' ' || lower(last_name) || ' ' || lower(email) LIKE ? OR qd_number::text LIKE ?", "%#{term.downcase}%", "%#{term}%") }
scope :customer, -> (customer_id) { where customer_id: customer_id }
I would start with something like this:
def index
if params[:page].present?
#proposals = current_user.retailer.proposals.page(params[:page])
#proposals = #proposals.customer(params[:customer_id]) if params[:customer_id].present?
#proposals = #proposals.search(params.slice(:filter)) if params[:filter].present?
render json: #proposals, root: 'proposals', meta: pagination_dict(#proposals)
else
render status: :bad_request, json: { message: "Please supply page parameter" }
end
end
Furthermore you might want to handle the error in a before_action:
before_action :check_required_parameters, only: :index
def index
#proposals = current_user.retailer.proposals.page(params[:page])
#proposals = #proposals.customer(params[:customer_id]) if params[:customer_id].present?
#proposals = #proposals.search(params.slice(:filter)) if params[:filter].present?
render json: #proposals, root: 'proposals', meta: pagination_dict(#proposals)
end
private
def check_required_parameters
return if params[:page].present?
render status: :bad_request, json: { message: "Please supply page parameter" }
end
Or you might want to change your scopes to handle blank values:
# in the model
scope :filter, -> (term) { where("lower(first_name) || ' ' || lower(last_name) || ' ' || lower(email) LIKE ? OR qd_number::text LIKE ?", "%#{term.downcase}%", "%#{term}%") if term.present? }
scope :customer, -> (customer_id) { where(customer_id: customer_id) if customer_id.present? }
# in the controller
def index
if params[:page].present?
#proposals = current_user.retailer.proposals
.customer(params[:customer_id])
.search(params.slice(:filter))
.page(params[:page])
render json: #proposals, root: 'proposals', meta: pagination_dict(#proposals)
else
render status: :bad_request, json: { message: "Please supply page parameter" }
end
end
Sometime one in a long series of events within a controller action fails. For example, a credit card is processed but then an ActiveRecord query times out. Is there any way to make those calls reversible?
E.g. with this controller action:
def process_order
cart = Cart.new(params[:cart])
load_order
response = credit_card.charge
if response
submit_order
order.receipt = Pdf.new(render_to_string(:partial => 'receipt')
order.receipt.pdf.generate
order.receipt.save
render :action => 'finished'
else
order.transaction = response
#message = order.transaction.message
order.transaction.save
render :action => 'charge_failed'
end
end
I would like to be able to put a block around it like so:
def process_order
transaction
cart = Cart.new(params[:cart])
load_order
response = credit_card.charge
if response
submit_order
order.receipt = Pdf.new(render_to_string(:partial => 'receipt')
order.receipt.pdf.generate
order.receipt.save
render :action => 'finished'
else
order.transaction = response
#message = order.transaction.message
order.transaction.save
render :action => 'charge_failed'
end
rollback
credit_card.cancel_charge
...
end
end
This is just a contrived example and I'm not really sure how it would work in practice. What typically happens is we get an exception like ActiveRecord::StatementInvalid: : execution expired for the line with submit_order and then we have to go and manually run the rest of the lines that should have run.
Here's a generic solution.
class Transactable
def initialize(&block)
raise LocalJumpError unless block_given?
#block = block
end
def on_rollback(&block)
raise LocalJumpError unless block_given?
#rollback = block
self
end
def call
#block.call
end
def rollback
#rollback.call if #rollback
end
end
class Transaction
def initialize(tasks)
tasks = Array(tasks)
tasks.each do |t|
Transactable === t or raise TypeError
end
#tasks = tasks
end
def run
finished_tasks = []
begin
#tasks.each do |t|
t.call
finished_tasks << t
end
rescue => err
finished_tasks.each do |t|
t.rollback
end
raise err
end
end
end
if __FILE__ == $0
Transaction.new([
Transactable.new { puts "1: call" }.on_rollback { puts "1: rollback" },
Transactable.new { puts "2: call" }.on_rollback { puts "2: rollback" },
Transactable.new { puts "3: call"; raise "fail!" }.on_rollback { puts "3: rollback" },
]).run
end
Note that it doesn't:
handle errors in the rollback block
call the rollback for the failed task, but that's easy to adjust
Just wrap it in
cart.transaction do
# ...
end
to use transactions. For details see http://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
I'm a bit late but I think you should use save! instead of save. save just returns false if something fails within your model but save! raises an exception and your ActiveRecord::Base.transaction do block rolls back your changes correctly...
For example:
def process_order
ActiveRecord::Base.transaction do
begin
cart = Cart.new(params[:cart])
load_order
response = credit_card.charge
if response
submit_order
order.receipt = Pdf.new(render_to_string(:partial => 'receipt')
order.receipt.pdf.generate
order.receipt.save!
render :action => 'finished'
else
order.transaction = response
#message = order.transaction.message
order.transaction.save!
render :action => 'charge_failed'
end
rescue
# Exception raised ... ROLLBACK
raise ActiveRecord::Rollback
end
end