pablocrivella/complicode

View on GitHub
lib/complicode/generator.rb

Summary

Maintainability
A
0 mins
Test Coverage
# frozen_string_literal: true

module Complicode
  class Generator
    BASE64 = %w[
      0 1 2 3 4 5 6 7 8 9 A B C D E F G H I J K L M N O P Q R S T U V
      W X Y Z a b c d e f g h i j k l m n o p q r s t u v w x y z + /
    ].freeze

    # @param authorization_code [String]
    # @param key [String]
    # @param invoice [Complicode::Invoice]
    # @return [String]
    def call(authorization_code:, key:, invoice:)
      key.freeze

      seeds = [
        invoice.number.to_s,
        invoice.nit.to_s,
        invoice.issue_date.strftime("%Y%m%d"),
        invoice.amount.round.to_s,
      ]

      seeds = append_verification_digits(seeds, 2)
      digits = generate_verification_digits(seeds)
      partial_keys = generate_partial_keys(key.dup, digits)

      seeds.unshift(authorization_code)
      seeds = append_partial_keys(seeds, partial_keys)

      encryption_key = key + digits
      encrypted_data = encrypt(seeds.join, encryption_key)

      ascii_sums = generate_ascii_sums(encrypted_data, partial_keys.count)
      base64_data = generate_base64_data(ascii_sums, partial_keys)

      format(encrypt(base64_data, encryption_key))
    end

    def append_verification_digits(seed, count)
      case seed
      when Array
        seed.map { |seed| append_verification_digits(seed, count) }
      else
        count.times do
          seed = seed.to_s + Verhoeff.checksum_digit_of(seed).to_s
        end

        seed
      end
    end

    # @param seeds [Array<String>]
    # @return [String]
    def generate_verification_digits(seeds)
      sum = seeds.map(&:to_i).sum
      sum = append_verification_digits(sum, 5)
      sum.to_s[-5..-1]
    end

    # @param key [String]
    # @param digits [String]
    # @return [Array<Complicode::PartialKey>]
    def generate_partial_keys(key, digits)
      partial_key_sizes = digits.split("").map { |digit| digit.to_i + 1 }
      partial_key_sizes.map { |index| PartialKey.new(value: key.slice!(0...index)) }
    end

    def append_partial_keys(seeds, partial_keys)
      seeds.map.with_index { |seed, index| seed + partial_keys[index].value }
    end

    # @param data [String]
    # @param partials_count [Integer]
    # @return [Struct]
    def generate_ascii_sums(data, partials_count)
      Struct.new(:total, :partials).new(0, Array.new(partials_count, 0)).tap do |sums|
        data.each_byte.with_index do |byte, index|
          sums.total += byte
          sums.partials[index % partials_count] += byte
        end
      end
    end

    # @param ascii_sums [Struct]
    # @param partial_keys [Array<String>]
    # @return [String]
    def generate_base64_data(ascii_sums, partial_keys)
      ascii_sums.partials.each_with_index.inject(0) { |sum, (partial_sum, index)|
        sum + ascii_sums.total * partial_sum / partial_keys[index].size
      }.b(10).to_s(BASE64)
    end

    # @param data [String]
    # @param encryption_key [String]
    # @return [String]
    def encrypt(data, encryption_key)
      RC4.new(encryption_key).encrypt(data).unpack1("H*").upcase
    end

    # @param code [String]
    # @return [String]
    def format(code)
      code.scan(/.{2}/).join("-")
    end
  end
end