sharetribe/sharetribe

View on GitHub
app/controllers/preauthorize_transactions_controller.rb

Summary

Maintainability
C
1 day
Test Coverage
class PreauthorizeTransactionsController < ApplicationController

  before_action do |controller|
   controller.ensure_logged_in t("layouts.notifications.you_must_log_in_to_do_a_transaction")
  end

  before_action :ensure_listing_is_open
  before_action :ensure_listing_author_is_not_current_user
  before_action :ensure_authorized_to_reply
  before_action :ensure_can_receive_payment

  def initiate
    params_validator = params_per_hour? ? TransactionService::Validation::NewPerHourTransactionParams : TransactionService::Validation::NewTransactionParams
    validation_result = params_validator.validate(params.to_unsafe_hash).and_then { |params_entity|
      tx_params = add_defaults(
        params: params_entity,
        shipping_enabled: listing.require_shipping_address,
        pickup_enabled: listing.pickup_enabled)
      tx_params[:marketplace_id] = @current_community.id

      TransactionService::Validation::Validator.validate_initiate_params(
        marketplace_uuid: @current_community.uuid_object,
        listing: listing,
        tx_params: tx_params,
        quantity_selector: listing.quantity_selector&.to_sym,
        shipping_enabled: listing.require_shipping_address,
        pickup_enabled: listing.pickup_enabled,
        availability_enabled: listing.availability.to_sym == :booking,
        stripe_in_use: StripeHelper.user_and_community_ready_for_payments?(listing.author_id, @current_community.id))
    }

    if validation_result.success
      initiation_success(validation_result.data)
    else
      initiation_error(validation_result.data)
    end
  end

  def initiated
    params_validator = params_per_hour? ? TransactionService::Validation::NewPerHourTransactionParams : TransactionService::Validation::NewTransactionParams
    validation_result = params_validator.validate(params.to_unsafe_hash).and_then { |params_entity|
      tx_params = add_defaults(
        params: params_entity,
        shipping_enabled: listing.require_shipping_address,
        pickup_enabled: listing.pickup_enabled)

      TransactionService::Validation::Validator.validate_initiated_params(
        tx_params: tx_params,
        marketplace_uuid: @current_community.uuid_object,
        listing: listing,
        quantity_selector: listing.quantity_selector&.to_sym,
        shipping_enabled: listing.require_shipping_address,
        availability_enabled: listing.availability.to_sym == :booking,
        pickup_enabled: listing.pickup_enabled,
        transaction_agreement_in_use: @current_community.transaction_agreement_in_use?,
        stripe_in_use: StripeHelper.user_and_community_ready_for_payments?(listing.author_id, @current_community.id))
    }

    if validation_result.success
      initiated_success(validation_result.data)
    else
      initiated_error(validation_result.data)
    end
  end

  def stripe_confirm_intent
    tx = Transaction.where(community: @current_community).find(params[:id])
    unless tx.participations.include?(@current_user)
      return
    end

    stripe_payment = tx.stripe_payments.find(params[:stripe_payment_id])

    begin
      intent = StripeService::API::StripeApiWrapper.confirm_payment_intent(
        community: @current_community,
        payment_intent_id: params[:payment_intent_id])
    rescue Stripe::CardError => e
      stripe_payment.update(stripe_payment_intent_status: StripePayment::PAYMENT_INTENT_FAILED)
      TransactionService::StateMachine.transition_to(tx.id, :payment_intent_failed)
      return render json: { error: t("error_messages.stripe.generic_error") }
    end

    if intent.status == StripePayment::PAYMENT_INTENT_REQUIRES_CAPTURE
      stripe_charge = intent['charges']['data'].first
      stripe_payment.update(stripe_charge_id: stripe_charge.id)
      TransactionService::StateMachine.transition_to(tx.id, :preauthorized)
      render json: {
        success: true,
        redirect_url: person_transaction_path(@current_user, params[:id])
      }
    else
      # Invalid status
      stripe_payment.update(stripe_payment_intent_status: StripePayment::PAYMENT_INTENT_INVALID)
      render json: { error: 'Invalid PaymentIntent status' }, status: :internal_server_error
    end
  end

  def stripe_failed_intent
    tx = Transaction.where(community: @current_community).find(params[:id])
    unless tx.participations.include?(@current_user)
      return
    end

    stripe_payment = tx.stripe_payments.find(params[:stripe_payment_id])
    stripe_payment.update(stripe_payment_intent_status: StripePayment::PAYMENT_INTENT_FAILED)
    TransactionService::StateMachine.transition_to(tx.id, :payment_intent_failed)
    render json: {
      success: true
    }
  end

  private

  def add_defaults(params:, shipping_enabled:, pickup_enabled:)
    default_shipping =
      case [shipping_enabled, pickup_enabled]
      when [true, false]
        {delivery: :shipping}
      when [false, true]
        {delivery: :pickup}
      when [false, false]
        {delivery: nil}
      else
        {}
      end

    params.merge(default_shipping)
  end

  def handle_tx_response(tx_response, gateway)
    if !tx_response[:success]
      render_error_response(request.xhr?, error_message(tx_response, gateway), action: :initiate)
    elsif tx_response[:data][:gateway_fields][:redirect_url]
      xhr_json_redirect tx_response[:data][:gateway_fields][:redirect_url]
    elsif gateway == :stripe
      handle_tx_stripe_payment_intent(tx_response)
    else
      render json: {
        op_status_url: transaction_op_status_path(tx_response[:data][:gateway_fields][:process_token]),
        op_error_msg: t("error_messages.#{gateway}.generic_error")
      }
    end
  end

  def handle_tx_stripe_payment_intent(tx_response)
    tx = tx_response.data[:transaction]
    stripe_payment = tx.stripe_payments.last
    if stripe_payment.stripe_payment_intent_id.present? && stripe_payment.stripe_payment_intent_client_secret.present?
      return render json: {
        stripe_payment_intent: {
          stripe_payment_id: stripe_payment.id,
          requires_action: true,
          client_secret: stripe_payment.stripe_payment_intent_client_secret,
          confirm_intent_path: stripe_confirm_intent_listing_preauthorize_transaction_path(listing.id, tx.id),
          failed_intent_path: stripe_failed_intent_listing_preauthorize_transaction_path(listing.id, tx.id)
        }
      }
    end
    xhr_json_redirect person_transaction_path(@current_user, tx_response[:data][:transaction][:id])
  end

  def xhr_json_redirect(redirect_url)
    if request.xhr?
      render json: { redirect_url: redirect_url }
    else
      redirect_to redirect_url
    end
  end

  def error_path(tx_params)
    booking_dates = HashUtils.map_values(tx_params.slice(:start_on, :end_on).compact) { |date|
      TransactionViewUtils.stringify_booking_date(date)
    }

    {action: :initiate}.merge(booking_dates)
  end

  def render_error_response(is_xhr, error_msg, redirect_params)
    if is_xhr
      render json: { error_msg: error_msg, location: redirect_params }
    else
      flash[:error] = error_msg
      redirect_to(redirect_params)
    end
  end

  def ensure_listing_author_is_not_current_user
    if listing.author == @current_user
      flash[:error] = t("layouts.notifications.you_cannot_send_message_to_yourself")
      redirect_to(session[:return_to_content] || search_path)
    end
  end

  # Ensure that only users with appropriate visibility settings can reply to the listing
  def ensure_authorized_to_reply
    unless Policy::ListingPolicy.new(listing, @current_community, @current_user).visible?
      flash[:error] = t("layouts.notifications.you_are_not_authorized_to_view_this_content")
      redirect_to search_path
    end
  end

  def ensure_listing_is_open
    if listing.closed?
      flash[:error] = t("layouts.notifications.you_cannot_reply_to_a_closed_offer")
      redirect_to(session[:return_to_content] || search_path)
    end
  end

  def listing
    @listing ||= Listing.find_by(
      id: params[:listing_id], community_id: @current_community.id) or render_not_found!("Listing #{params[:listing_id]} not found from community #{@current_community.id}")
  end

  def ensure_can_receive_payment
    payment_type = @current_community.active_payment_types || :none

    ready = TransactionService::Transaction.can_start_transaction(transaction: {
        payment_gateway: payment_type,
        community_id: @current_community.id,
        listing_author_id: listing.author.id
      })

    unless ready[:data][:result]
      flash[:error] =
        if @current_community.allow_free_conversations?
          t("layouts.notifications.listing_author_payment_details_missing")
        else
          t("layouts.notifications.listing_author_payment_details_missing_no_free")
        end

      record_event(
        flash,
        "ProviderPaymentDetailsMissing",
        { listing_id: listing.id,
          listing_uuid: listing.uuid_object.to_s })

      redirect_to listing_path(listing)
    end
  end

  def create_preauth_transaction(opts)
    case opts[:payment_type].to_sym
    when :paypal
      # PayPal doesn't like images with cache buster in the URL
      logo_url = Maybe(opts[:community])
               .wide_logo
               .select { |wl| wl.present? }
               .url(:paypal, timestamp: false)
               .or_else(nil)

      gateway_fields =
        {
          merchant_brand_logo_url: logo_url,
          success_url: success_paypal_service_checkout_orders_url,
          cancel_url: cancel_paypal_service_checkout_orders_url(listing_id: opts[:listing].id)
        }
    when :stripe
      gateway_fields =
        {
          stripe_email: @current_user.primary_email.address,
          stripe_token: params[:stripe_token],
          shipping_address: params[:shipping_address],
          service_name: @current_community.name_with_separator(I18n.locale),
          stripe_payment_method_id: params[:stripe_payment_method_id]
        }
    end

    transaction = {
          community_id: opts[:community].id,
          community_uuid: opts[:community].uuid_object,
          listing_id: opts[:listing].id,
          listing_uuid: opts[:listing].uuid_object,
          listing_title: opts[:listing].title,
          starter_id: opts[:user].id,
          starter_uuid: opts[:user].uuid_object,
          listing_author_id: opts[:listing].author.id,
          listing_author_uuid: opts[:listing].author.uuid_object,
          listing_quantity: opts[:listing_quantity],
          unit_type: opts[:listing].unit_type,
          unit_price: opts[:listing].price,
          unit_tr_key: opts[:listing].unit_tr_key,
          unit_selector_tr_key: opts[:listing].unit_selector_tr_key,
          availability: opts[:listing].availability,
          content: opts[:content],
          payment_gateway: opts[:payment_type].to_sym,
          payment_process: :preauthorize,
          booking_fields: opts[:booking_fields],
          delivery_method: opts[:delivery_method] || :none
    }

    if opts[:delivery_method] == :shipping
      transaction[:shipping_price] = opts[:shipping_price]
    end
    TransactionService::Transaction.create({
        transaction: transaction,
        gateway_fields: gateway_fields
      },
      force_sync: opts[:payment_type] == :stripe || opts[:force_sync])
  end

  def paypal_event_params(listing)
    [
      "RedirectingBuyerToPayPal",
      {
        listing_id: listing.id,
        listing_uuid: listing.uuid_object.to_s,
        community_id: @current_community.id,
        marketplace_uuid: @current_community.uuid_object.to_s,
        user_logged_in: @current_user.present?
      }
    ]
  end

  def params_per_hour?
    params[:per_hour] == '1'
  end

  def initiation_success(tx_params)
    record_event(
      flash.now,
      "InitiatePreauthorizedTransaction",
      { listing_id: listing.id,
        listing_uuid: listing.uuid_object.to_s })

    order = TransactionService::Order.new(
      community: @current_community,
      tx_params: tx_params,
      listing: listing)

    render "listing_conversations/initiate",
           locals: {
             start_on: tx_params[:start_on],
             end_on: tx_params[:end_on],
             start_time: tx_params[:start_time],
             end_time: tx_params[:end_time],
             per_hour: tx_params[:per_hour],
             listing: listing,
             delivery_method: tx_params[:delivery],
             quantity: tx_params[:quantity],
             author: listing.author,
             action_button_label: translate(listing.action_button_tr_key),
             paypal_in_use: order.paypal_in_use,
             paypal_expiration_period: TransactionService::Transaction.authorization_expiration_period(:paypal),
             stripe_in_use: order.stripe_in_use,
             stripe_publishable_key: StripeHelper.publishable_key(@current_community.id),
             stripe_shipping_required: listing.require_shipping_address && tx_params[:delivery] != :pickup,
             form_action: initiated_order_path(person_id: @current_user.id, listing_id: listing.id),
             country_code: LocalizationUtils.valid_country_code(@current_community.country),
             paypal_analytics_event: paypal_event_params(listing),
             price_break_down_locals: order.price_break_down_locals
           }
  end

  def initiation_error(data)
    error_msg =
      if data.is_a?(Array)
        # Entity validation failed
        t("listing_conversations.preauthorize.invalid_parameters")
      elsif [:dates_missing,
             :end_cant_be_before_start,
             :at_least_one_day_or_night_required,
             :date_too_late
            ].include?(data[:code])
        t("listing_conversations.preauthorize.invalid_parameters")
      elsif data[:code] == :delivery_method_missing
        t("listing_conversations.preauthorize.select_delivery_method")
      elsif data[:code] == :dates_not_available
        t("listing_conversations.preauthorize.dates_not_available")
      else
        raise NotImplementedError.new("No error handler for: #{msg}, #{data.inspect}")
      end

    flash[:error] = error_msg
    logger.error(error_msg, :transaction_initiate_error, data)
    redirect_to listing_path(listing.id)
  end

  def initiated_success(tx_params)
    order = TransactionService::Order.new(
      community: @current_community,
      tx_params: tx_params,
      listing: listing)

    tx_response = create_preauth_transaction(
      payment_type: params[:payment_type].to_sym,
      community: @current_community,
      listing: listing,
      listing_quantity: order.quantity,
      user: @current_user,
      content: tx_params[:message],
      force_sync: !request.xhr?,
      delivery_method: tx_params[:delivery],
      shipping_price: order.shipping_total,
      booking_fields: {
        start_on: tx_params[:start_on],
        end_on: tx_params[:end_on],
        start_time: tx_params[:start_time],
        end_time: tx_params[:end_time],
        per_hour: tx_params[:per_hour]
      })

    handle_tx_response(tx_response, params[:payment_type].to_sym)
  end

  def initiated_error(data)
    error_msg, path =
      if data.is_a?(Array)
        # Entity validation failed
        logger.error(msg, :transaction_initiated_error, data)
        [t("listing_conversations.preauthorize.invalid_parameters"), listing_path(listing.id)]

      elsif [:dates_missing, :end_cant_be_before_start, :delivery_method_missing, :at_least_one_day_or_night_required].include?(data[:code])
        logger.error(msg, :transaction_initiated_error, data)
        [t("listing_conversations.preauthorize.invalid_parameters"), listing_path(listing.id)]
      elsif data[:code] == :agreement_missing
        # User error, no logging here
        [t("error_messages.transaction_agreement.required_error"), error_path(data[:tx_params])]
      elsif data[:code] == :dates_not_available
        [t("error_messages.booking.double_booking_payment_voided"), listing_path(listing.id)]
      else
        raise NotImplementedError.new("No error handler for: #{msg}, #{data.inspect}")
      end

    render_error_response(request.xhr?, error_msg, path)
  end

  def error_message(tx_response, gateway)
    translated_stripe_error_codes = %w(card_declined expired_card)
    if tx_response[:data].is_a?(Stripe::CardError) &&
       translated_stripe_error_codes.include?(tx_response[:data].code)
      t("error_messages.stripe.#{tx_response[:data].code}")
    elsif tx_response[:data].is_a?(TransactionService::Transaction::BookingDatesInvalid)
      tx_response[:error_msg]
    else
      t("error_messages.#{gateway}.generic_error")
    end
  end
end