yurijmi/better_offsite_payments

View on GitHub
lib/offsite_payments/integrations/klarna.rb

Summary

Maintainability
A
0 mins
Test Coverage
module OffsitePayments #:nodoc:
  module Integrations #:nodoc:
    module Klarna
      mattr_accessor :service_url
      self.service_url = 'https://api.hostedcheckout.io/shopify/payment'

      REQUIRED_FIELDS = %w(amount checkout_token merchant_base_uri merchant_checkout_uri merchant_confirmation_uri merchant_id merchant_terms_uri purchase_currency)

      def self.notification(post_body, options = {})
        Notification.new(post_body, options)
      end

      def self.return(query_string, options = {})
        Return.new(query_string, options)
      end

      def self.cart_items_payload(fields, cart_items)
        check_required_fields!(fields)

        payload = ""
        REQUIRED_FIELDS.sort.each do |field|
           payload << fields[field].to_s
        end

        payload
      end

      def self.sign(fields, cart_items, shared_secret)
        payload = cart_items_payload(fields, cart_items)

        digest(payload, shared_secret)
      end

      def self.digest(payload, shared_secret)
        Digest::SHA256.base64digest(payload + shared_secret.to_s)
      end

      private

      def self.check_required_fields!(fields)
        REQUIRED_FIELDS.each do |required_field|
          raise ArgumentError, "Missing required field #{required_field}" if fields[required_field].nil?
        end
      end

      class Helper < OffsitePayments::Helper
        mapping :currency, 'purchase_currency'
        mapping :cancel_return_url, ['merchant_terms_uri', 'merchant_checkout_uri', 'merchant_base_uri']
        mapping :account, 'merchant_id'
        mapping :customer, email: 'shipping_address_email'
        mapping :checkout_token, 'checkout_token'
        mapping :amount, 'amount'

        def initialize(order, account, options = {})
          super
          @shared_secret = options[:credential2]
          @order = order

          add_field('platform_type', application_id)
          add_field('test_mode', test?.to_s)
        end

        def notify_url(url)
          url = append_order_query_param(url)
          add_field('merchant_push_uri', url)
        end

        def return_url(url)
          url = append_order_query_param(url)
          add_field('merchant_confirmation_uri', url)
        end

        def line_item(item)
          @line_items ||= []
          @line_items << item

          i = @line_items.size - 1

          add_field("cart_item-#{i}_type", type_for(item))
          add_field("cart_item-#{i}_reference", item.fetch(:reference, ''))
          add_field("cart_item-#{i}_name", item.fetch(:name, ''))
          add_field("cart_item-#{i}_quantity", item.fetch(:quantity, ''))
          add_field("cart_item-#{i}_unit_price", tax_included_unit_price(item)).to_s
          add_field("cart_item-#{i}_discount_rate", item.fetch(:discount_rate, ''))
          add_field("cart_item-#{i}_tax_rate", tax_rate_for(item)).to_s

          @fields
        end

        def billing_address(billing_fields)
          country = billing_fields[:country]

          add_field('purchase_country', country)
          add_field('locale', guess_locale_based_on_country(country))
        end

        def shipping_address(shipping_fields)
          add_field('shipping_address_given_name', shipping_fields[:first_name])
          add_field('shipping_address_family_name', shipping_fields[:last_name])

          street_address = [shipping_fields[:address1], shipping_fields[:address2]].compact.join(', ')
          add_field('shipping_address_street_address', street_address)

          add_field('shipping_address_postal_code', shipping_fields[:zip])
          add_field('shipping_address_city', shipping_fields[:city])
          add_field('shipping_address_country', shipping_fields[:country])
          add_field('shipping_address_phone', shipping_fields[:phone])
        end

        def form_fields
          sign_fields

          super
        end

        def sign_fields
          merchant_digest = Klarna.sign(@fields, @line_items, @shared_secret)
          add_field('merchant_digest', merchant_digest)
        end

        private

        def type_for(item)
          case item.fetch(:type, '')
          when 'shipping'
            'shipping_fee'
          when 'line item'
            'physical'
          when 'discount'
            'discount'
          else
            raise StandardError, "Unable to determine type for item #{item.to_yaml}"
          end
        end

        def append_order_query_param(url)
          u = URI.parse(url)
          params = Rack::Utils.parse_nested_query(u.query)
          params["order"] = @order
          u.query = params.to_query

          u.to_s
        end

        def guess_locale_based_on_country(country_code)
          case country_code
          when /no/i
            "nb-no"
          when /fi/i
            "fi-fi"
          when /se/i
            "sv-se"
          else
            "sv-se"
          end
        end

        def tax_included_unit_price(item)
          item.fetch(:unit_price, '').to_i + item.fetch(:tax_amount, '').to_i
        end

        def tax_rate_for(item)
          subtotal_price = item.fetch(:unit_price, 0).to_f * item.fetch(:quantity, 0).to_i
          tax_amount = item.fetch(:tax_amount, 0).to_f

          if subtotal_price > 0
            tax_rate = tax_amount / subtotal_price
            tax_rate = tax_rate.round(4)

            percentage_to_two_decimal_precision_whole_number(tax_rate)
          else
            0
          end
        end

        def percentage_to_two_decimal_precision_whole_number(percentage)
          (percentage * 10000).to_i
        end
      end

      class Notification < OffsitePayments::Notification
        def initialize(post, options = {})
          super
          @shared_secret = @options[:credential2]
        end

        def complete?
          status == 'Completed'
        end

        def item_id
          order
        end

        def transaction_id
          params["reference"]
        end

        def received_at
          params["completed_at"]
        end

        def payer_email
          params["billing_address"]["email"]
        end

        def receiver_email
          params["shipping_address"]["email"]
        end

        def currency
          params["purchase_currency"].upcase
        end

        def gross
          amount = Float(gross_cents) / 100
          sprintf("%.2f", amount)
        end

        def gross_cents
          params["order_amount"]
        end

        def status
          case params['status']
          when 'checkout_complete'
            'Completed'
          else
            params['status']
          end
        end

        def acknowledge(authcode = nil)
          Verifier.new(@options[:authorization_header], @raw, @shared_secret).verify
        end

        private

        def order
          query = Rack::Utils.parse_nested_query(@options[:query_string])
          query["order"]
        end

        def parse(post)
          @raw = post.to_s
          @params = JSON.parse(post)
        end

        class Verifier
          attr_reader :header, :payload, :digest, :shared_secret
          def initialize(header, payload, shared_secret)
            @header, @payload, @shared_secret = header, payload, shared_secret

            @digest = extract_digest
          end

          def verify
            digest_matches?
          end

          private

          def extract_digest
            match = header.match(/^Klarna (?<digest>.+)$/)
            match && match[:digest]
          end

          def digest_matches?
            Klarna.digest(payload, shared_secret) == digest
          end
        end
      end
    end
  end
end