SumOfUs/Champaign

View on GitHub
app/models/payment/braintree.rb

Summary

Maintainability
A
3 hrs
Test Coverage
# frozen_string_literal: true

module Payment::Braintree
  class SelectPaymentType
    PAYPAL_IDENTIFIER = 'PYPL'
    GIRO_IDENTIFIER = 'GIRO'
    IDEAL_IDENTIFIER = 'IDEL'

    def initialize(transaction)
      @transaction = transaction
    end

    def select
      if is_paypal?
        PAYPAL_IDENTIFIER
      elsif is_local?
        select_local_type
      else
        @transaction.credit_card_details.last_4
      end
    end

    def is_local?
      @transaction.payment_instrument_type.inquiry.local_payment?
    end

    def select_local_type
      source = @transaction.local_payment_details.funding_source

      result = {
        'giropay' => GIRO_IDENTIFIER,
        'ideal' => IDEAL_IDENTIFIER
      }[source]

      result
    end

    def is_paypal?
      @transaction.payment_instrument_type.inquiry.paypal_account?
    end
  end

  class << self
    def table_name_prefix
      'payment_braintree_'
    end

    # TODO: Why don't we have an options hash at the end?
    # def write_transaction(bt_result, page_id, member_id, existing_customer, options = {})
    def write_transaction(bt_result, page_id, member_id, existing_customer, save_customer = true, store_in_vault: false)
      BraintreeTransactionBuilder.build(bt_result, page_id, member_id, existing_customer, save_customer, store_in_vault: store_in_vault)
    end

    def write_subscription(payment_method_id, customer_id, subscription_result, page_id, action_id, currency)
      if subscription_result.success?
        Payment::Braintree::Subscription.create(payment_method_id: payment_method_id,
                                                customer_id: customer_id,
                                                subscription_id: subscription_result.subscription.id,
                                                amount: subscription_result.subscription.price,
                                                merchant_account_id: subscription_result.subscription.merchant_account_id,
                                                billing_day_of_month: subscription_result.subscription.billing_day_of_month,
                                                action_id: action_id,
                                                currency: currency,
                                                page_id: page_id)
      end
    end

    def write_customer(bt_customer, bt_payment_method, member_id, existing_customer)
      BraintreeCustomerBuilder.build(bt_customer, bt_payment_method, member_id, existing_customer)
    end

    def customer(email)
      member = Member.find_by_email(email)
      member.try(:customer)
    end
  end

  class BraintreeCustomerBuilder
    def self.build(bt_customer, bt_payment_method, member_id, existing_customer, store_in_vault: false)
      new(bt_customer, bt_payment_method, member_id, existing_customer, store_in_vault).build
    end

    def initialize(bt_customer, bt_payment_method, member_id, existing_customer, store_in_vault)
      @bt_customer = bt_customer
      @customer = existing_customer
      @member_id = member_id
      @bt_payment_method = bt_payment_method
      @store_in_vault = store_in_vault
    end

    def build
      if @customer.present?
        @customer.update(customer_attrs)
      else
        @customer = Payment::Braintree::Customer.create(customer_attrs)
      end

      payment_method = Payment::Braintree::PaymentMethod.find_or_create_by!(token: @bt_payment_method.token) do |pm|
        pm.customer = @customer
        pm.store_in_vault = @store_in_vault
      end

      case @bt_payment_method
      when Braintree::PayPalAccount
        payment_method.update(email: @bt_payment_method.email,
                              instrument_type: 'paypal_account')
      when Braintree::CreditCard
        payment_method.update(instrument_type: 'credit_card',
                              last_4: @bt_payment_method.last_4,
                              bin: @bt_payment_method.bin,
                              expiration_date: @bt_payment_method.expiration_date,
                              card_type: @bt_payment_method.card_type,
                              cardholder_name: @bt_payment_method.cardholder_name)
      end

      @customer
    end

    def customer_attrs
      card_attrs.merge(customer_id: @bt_customer.id,
                       member_id: @member_id,
                       email: @bt_customer.email)
    end

    def card_attrs
      if @bt_payment_method.is_a? Braintree::CreditCard
        @bt_payment_method.instance_eval do
          {
            card_type: card_type,
            card_bin: bin,
            cardholder_name: cardholder_name,
            card_debit: debit,
            card_last_4: last_4,
            card_unique_number_identifier: unique_number_identifier
          }
        end
      else
        {
          card_last_4: 'PYPL' # for now, assume PayPal if not CC
        }
      end
    end
  end

  class BraintreeTransactionBuilder
    #
    # Stores and associates a Braintree transaction as +Payment::BraintreeTransaction+. Builder will also
    # create or update an instance of +Payment::BraintreeCustomer+, if save_customer is passed
    #
    # === Options
    #
    # * +:bt_result+   - A Braintree::Transaction response object or a Braintree::Subscription response
    #                    (see https://developers.braintreepayments.com/reference/response/transaction/ruby)
    #                    or a Braintree::WebhookNotification
    # * +:page_id+     - the id of the Page to associate with the transaction record
    # * +:member_id+   - the member_id to associate with the customer record
    # * +:existing_customer+ - if passed, this customer is updated instead of creating a new one
    # * +:save_customer+     - optional, default true. whether to save the customer info too
    #
    #

    def self.build(bt_result, page_id, member_id, existing_customer, save_customer = true, store_in_vault = false)
      new(bt_result, page_id, member_id, existing_customer, save_customer, store_in_vault).build
    end

    def initialize(bt_result, page_id, member_id, existing_customer, save_customer = true, store_in_vault: false)
      @bt_result = bt_result
      @page_id = page_id
      @member_id = member_id
      @existing_customer = existing_customer
      @save_customer = save_customer
      @store_in_vault = store_in_vault
    end

    # NOTE this method has all the looks of a service:
    #  - create_customer
    #  - create_payment_method
    #  - create_transactions
    def build
      return unless transaction.present?

      create_customer
      create_payment_method
      record = create_transaction
      return false unless successful?

      @customer.update(customer_attrs) if @save_customer && @customer
      record
    end

    private

    def create_customer
      if transaction.customer_details.id
        @customer = @existing_customer || Payment::Braintree::Customer.find_or_create_by!(
          member_id: @member_id,
          customer_id: transaction.customer_details.id
        )
      end
    end

    def create_payment_method
      @local_payment_method_id = if payment_method_token.nil? || @bt_result.transaction.nil?
                                   nil
                                 else
                                   BraintreeServices::PaymentMethodBuilder.new(
                                     transaction: @bt_result.transaction,
                                     customer: @customer,
                                     store_in_vault: @store_in_vault
                                   ).create.id
                                 end
    end

    def create_transaction
      ::Payment::Braintree::Transaction.create!(transaction_attrs)
    end

    def transaction_attrs
      {
        transaction_id: transaction.id,
        transaction_type: transaction.type,
        payment_instrument_type: transaction.payment_instrument_type,
        amount: transaction.amount,
        transaction_created_at: transaction.created_at,
        merchant_account_id: transaction.merchant_account_id,
        processor_response_code: transaction.processor_response_code,
        currency: transaction.currency_iso_code,
        customer_id: @customer.try(:customer_id),
        status: status,
        payment_method_id: @local_payment_method_id,
        page_id: @page_id
      }.tap do |data|
        if transaction.try(:subscription_id)
          data[:subscription] = Payment::Braintree::Subscription.find_by_subscription_id(transaction.subscription_id)
        end
      end
    end

    def customer_attrs
      {
        # NOTE: we do NOT store card_unique_number_identifier because
        # that is only returned on Braintree::CreditCard, not on
        # Braintree::Transaction::CreditCardDetails
        card_type: card.card_type,
        card_bin: card.bin,
        cardholder_name: card.cardholder_name,
        card_debit: card.debit,
        card_last_4: last_4,
        customer_id: transaction.customer_details.id,
        email: transaction.customer_details.email,
        member_id: @member_id
      }
    end

    def transaction
      @bt_result.try(:transaction) || @bt_result.try(:subscription).try(:transactions).try(:first)
    end

    def card
      transaction.credit_card_details
    end

    def status
      Payment::Braintree::Transaction.statuses[(successful? ? :success : :failure)]
    end

    def successful?
      return @bt_result.success? if @bt_result.respond_to?(:success?)
      if @bt_result.is_a?(Braintree::WebhookNotification) && @bt_result.kind == 'subscription_charged_successfully'
        return true
      end

      false
    end

    def last_4
      ::Payment::Braintree::SelectPaymentType.new(transaction).select
    end

    def payment_method_token
      case transaction.payment_instrument_type
      when 'credit_card'
        transaction.credit_card_details.try(:token)
      when 'paypal_account'
        transaction.paypal_details.try(:token)
      end
    end
  end
end