Alex
Alex

Reputation: 41

Stripe webhook error 400 bad request when using Heroku web address

enter image description hereI have integrated Stripe in my Ruby on Rails website. If I test it with ngrok, everything works fine but when I use my heroku website address as stripe webhook, it throws a 400 bad request error. If I look up the documentation it says a required parameter is missing. Could this be because I don't have a ssl certificate? I am on the free tier on Heroku but nevertheless the heroku web address starts with https... is that not secure? I have entered the publishable, secret and signing key on the heroku website.

Routes.rb

Rails.application.routes.draw do

  mount StripeEvent::Engine, at: '/stripe-webhooks'

  devise_for :users, controllers: {
    sessions: 'users/sessions',
    passwords: 'users/passwords',
    registrations: 'users/registrations'
  }

  scope '(:locale)', locale: /en|de/ do
    root to: 'pages#home'
    get '/about', to: 'pages#about', as: 'about'
    get '/shipping', to: 'pages#shipping', as: 'shipping'
    get '/privacypolicy', to: 'pages#privacypolicy', as: 'privacypolicy'
    get '/termsandconditions', to: 'pages#termsandconditions', as: 'termsandconditions'
    get '/success', to: 'pages#success'

    get 'contact', to: 'contacts#new', as: 'contact'


    resources :contacts, only: [:new, :create]


    get 'cart', to: 'carts#show', as: 'cart'
    delete 'carts', to: 'carts#destroy'
    delete 'cart_items/:id', to: 'cart_items#destroy', as: 'cart_items'

    resources :cart_items, only: [:create, :destroy] do
      member do
        get :add_quantity
        get :reduce_quantity
      end
    end

    post 'without-login', to: 'orders#without_login'

    resources :users
    resources :products do
      resources :cart_items, only: [:create]
    end
    resources :categories
    resources :orders, only: [:new, :show, :create] do
      resources :payments, only: :new
    end
  end
end

schema:

create_table "orders", force: :cascade do |t|
    t.string "product_sku"
    t.integer "amount_cents", default: 0, null: false
    t.string "checkout_session_id"
    t.bigint "user_id"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.string "first_name"
    t.string "last_name"
    t.string "street_name"
    t.string "house_number"
    t.string "postal_code"
    t.string "city"
    t.string "country"
    t.string "email"
    t.text "comment"
    t.integer "price_cents", default: 0, null: false
    t.boolean "termsandconditions"
    t.string "status", default: "pending"
    t.index ["user_id"], name: "index_orders_on_user_id"
  end
class PaymentsController < ApplicationController
    skip_before_action :authenticate_user!

 def new
    if current_user
      @order = current_user.orders.where(status: 'pending').find(params[:order_id])
    else
      @order = Order.find(params[:order_id])
    end
    gon.order_amount = @order.amount_cents.to_f/100
    gon.order_id = @order.id
  end

end
class OrdersController < ApplicationController
  skip_before_action :authenticate_user!

  def new
    @order = Order.new
    @cart = current_cart
    # Storing the two constants in a gon variable to send data to the JS file
    gon.ceilings = Product::ORDER_CEILINGS
    gon.prices = Product::SHIPPING_PRICES
  end

  def create
    @order = Order.new(order_params)
    @cart = current_cart
    @cart.cart_items.each { |item| @order.cart_items << item }
    @order.amount = @cart.total_price

    shipping_costs = calculate_shipping_costs(params[:order][:country], @order.amount)
    @order.amount += Monetize.parse(shipping_costs)

    @order.user = current_user if current_user
    @order.email = current_user.email if current_user

    if @order.save
      save_user_address if params[:save_address].to_i == 1
      trigger_stripe(shipping_costs)
      cleanup_cart
      redirect_to new_order_payment_path(@order)
    else
      @cart = @current_cart
      render :new
    end
  end

  def show
    if current_user
      @order = current_user.orders.find(params[:id])
    else
      @order = Order.find(params[:id])
    end

    mail = OrderMailer.with(order: @order).confirmation
    mail.deliver_now
    # may need to change this for guest users- must check that their email address is saved to the database
  end

  def index
    @orders = current_user.orders
  end

  def without_login
    session[:without_login] = true
    redirect_to new_order_path
  end

  def submit
  end


  private

  def order_params
    params.require(:order).permit(:first_name, :last_name, :email, :street_name, :house_number, :postal_code, :city, :country, :comment)
  end

  def trigger_stripe(shipping_costs)
    stripe_session = Stripe::Checkout::Session.create(
      payment_method_types: ['card'],
      customer_email: customer_email,
      locale: I18n.locale.to_s,
      line_items: stripe_line_items(@order.cart_items, shipping_costs),
      success_url: order_url(@order),
      cancel_url: order_url(@order)
    )
    @order.update(checkout_session_id: stripe_session.id)
  end

  def cleanup_cart
    @cart.cart_items.each { |item| item.update(cart_id: nil) }
    Cart.destroy(session[:cart_id])
    session[:cart_id] = nil
  end

  def stripe_line_items(order_items, shipping_costs)
    all_items = []

    order_items.each do |item|
      item_hash = {
        name: item.product.title,
        amount: (item.total_price.amount * 100).to_i / item.quantity,
        quantity: item.quantity,
        currency: 'eur'
       }
       all_items.push(item_hash)
    end

    shipping_item_hash = {
      name: "Delivery",
      amount: (shipping_costs * 100).to_i,
      quantity: 1,
      currency: 'eur'
    }
    all_items.push(shipping_item_hash)

    return all_items
  end

  def customer_email
    current_user ? current_user.email : nil
  end

  def save_user_address
    if @order.user != nil
      current_user.attributes = @order.attributes.except("id", "email", "status", "comment", "amount_cents", "amount_currency", "checkout_session_id", "user_id", "updated_at", "created_at")
      current_user.save
    end
  end
class StripeCheckoutSessionService
  def call(event)
    order = Order.find_by(checkout_session_id: event.data.object.id)
    order.update(status: 'paid')
  end
end

payments new.html.erb:

<script src="https://js.stripe.com/v3/"></script>
<script>
  const paymentButton = document.getElementById('pay-stripe');
  paymentButton.addEventListener('click', () => {
    const stripe = Stripe('<%= ENV['STRIPE_PUBLISHABLE_KEY'] %>');
    stripe.redirectToCheckout({
      sessionId: '<%= @order.checkout_session_id %>'
    });
  });
  </script>

initializers stripe

Rails.configuration.stripe = {
  publishable_key: ENV['STRIPE_PUBLISHABLE_KEY'],
  secret_key:      ENV['STRIPE_SECRET_KEY'],
  signing_secret:  ENV['STRIPE_WEBHOOK_SECRET_KEY']
}

Stripe.api_key = Rails.configuration.stripe[:secret_key]

StripeEvent.signing_secret = Rails.configuration.stripe[:signing_secret]

StripeEvent.configure do |events|
  events.subscribe 'checkout.session.completed', StripeCheckoutSessionService.new
end

Upvotes: 3

Views: 4625

Answers (2)

Decoy
Decoy

Reputation: 393

I had a similar issue. l mistakenly put the testing key (which was there when I created the live key in the code sample beside it).

Open your webhooks in stripe and click on the reveal secret key. put this secret key in your webhook handler

Upvotes: 2

karbi
karbi

Reputation: 2173

It looks like StripeEvent responds with a 400 error if it gets back a Signature Verification Error from Stripe (see https://github.com/integrallis/stripe_event/blob/31b948d6afd4a2f82c6ad3cd973211366b48a0d8/app/controllers/stripe_event/webhook_controller.rb#L12).

You should double check your signing secret and make sure it matches the secret for your heroku webhook, and not your ngrok webhook.

Upvotes: 2

Related Questions