terça-feira, 8 de setembro de 2015

Integração entre Rails e Iugu

Já faz algum tempo que comecei uma integração entre uma aplicação Rails e o iugu. Postei no grupo Ruby on Rails Brasil a respeito da abordagem que estou utilizando, tanto para compartilhar a ideia, quanto para obter um feedback para tentar deixar a coisa mais simples.

Neste meio tempo, um aspecto desta integração começou a me incomodar, então resolvi fazer este post para comentar sobre isto e também compartilhar partes do meu código a pedido de alguns usuários do post no face. Quem sabe agora eu recebo algum feedback!!! :-)

Meu modelo User estava com o seguinte código:

  after_create :create_iugu_customer
  after_update :update_iugu_customer, if: 'name_changed? or email_changed?'

  private

  def create_iugu_customer
    iugu_customer = Iugu::Customer.create({
      email: self.email,
      name:  self.name
    })
    self.update_column(:iugu_customer_id, iugu_customer.id)
  end

  def update_iugu_customer
    customer = Iugu::Customer.fetch(self.iugu_customer_id)
    customer.email = self.email
    customer.name  = self.name
    customer.save
  end


Pode ser apenas paranóia minha, mas esse é um tipo de código que me incomoda, pois vou precisar de um usuário para muitos testes do meu sistema, porém para a grande maioria deles não preciso de um Iugu::Customer. Ter meu teste conversando com uma API externa também me incomoda, para este último caso posso utilizar o VCR, porém imagine adicionar o VCR a diversos testes para utilizar a rede sendo que você não precisa do Iugu::Customer, incoerente não?

Pensando nisso googlei um pouco e li sobre algumas alternativas, tal como desligar callbacks durante a execução dos testes, criar um camada de serviço, etc. A mais coerente para mim era criar uma camada de serviço, porém analisando os requisitos da aplicação achei que uma complexidade desnecessária. Optei por criar o Iugu::Customer apenas quando necessário.

Minha feature ficou então da seguinte forma:

require 'rails_helper'

feature "Payment" do
  include ActiveJob::TestHelper
  let(:today) { Date.today }

  scenario "I should see a link to pay the service annuity", vcr: { cassette_name: 'iugu' } do
    # Given an event
    user = create(:user_confirmed)
    perform_enqueued_jobs do
      create_service(user)
      simulate_iugu_controller_webhook('invoice.created')
    end
    expect(page).to have_content('Você possui 30 dias para experimentar nosso sistema, aproveite!')
    expect(page).to have_content('Gostou do serviço? Contrate agora!')
    
    # When the user ask to contract the service
    click_link 'Contrate agora'

    # Then I can view my invoice with a payment link
    within '.box#invoices' do
      expect(page).to have_content('UUID')
      expect(page).to have_content('Anuidade serviço BLAH BLAH BLAH')
      expect(page).to have_content(I18n.l(today + 30.days))
      expect(page).to have_content('Pendente')
      expect(page).to have_content('R$ 999,99')
      expect(page).to have_xpath("//a[@href='https://iugu.com/invoices/uuid']")
    end
  end

  private

  def simulate_iugu_controller_webhook(event, data = {})
    data[:id]              ||= 'INVOICE_UUID'
    data[:status]          ||= 'pending'
    data[:subscription_id] ||= 'SUBSCRIPTION_UUID'

    Webhooks::Iugu::InvoiceCreationJob.perform_later(data)     and return if event == 'invoice.created'
    Webhooks::Iugu::InvoiceUpdateStatusJob.perform_later(data) and return if event == 'invoice.status_changed'

    raise "Iugu event not recognized: #{event}"
  end
end


No post do face falei que utilizei um request spec, porém posso simular o comportamento do controlador em um feature spec, como pode ser visto acima. Para gravar toda a interação com o VCR é necessário utilizar a opção "record: :new_episodes", completar o UUID retornado pelo iugu e ir executando os testes até ter todos os dados da interação. Além do próprio cassette você pode utilizar o painel de controle do iugu e o ngrok para conseguir mais detalhes da interação.

Observe no começo do cenário que utilizei o método perform_enqueued_jobs, pois após a criação do serviço eu executo um JOB para criação de uma Iugu::Subscription:

class Service < ActiveRecord::Base
  after_commit :schedule_subscription_creation, on: :create

  private

  def schedule_subscription_creation
    SubscriptionCreationJob.perform_later(self)
  end
end


O conteúdo deste JOB é o seguinte:

class SubscriptionCreationJob < ActiveJob::Base
  queue_as :default

  def perform(service)
    @user = service.owners.first
    @user.iugu_customer_id.blank? ? create_iugu_customer : update_iugu_customer
    iugu_subscription = Iugu::Subscription.create({
      plan_identifier: "basic_plan",
      customer_id: @user.iugu_customer_id,
      expires_at: 30.days.from_now,
      subitems: [{
        description: "Anuidade do serviço BLAH BLAH BLAH",
        price_cents: '999999',
        quantity: 1,
        recurrent: true,
      }],
      custom_variables: [{
        name: 'service_id',
        value: service.id
      }]
    })
    service.create_subscription(iugu_id: iugu_subscription.id, iugu_attributes: iugu_subscription.attributes)
  end

  private

  def create_iugu_customer
    iugu_customer = Iugu::Customer.create({
      email: @user.email,
      name:  @user.name
    })
    @user.update_column(:iugu_customer_id, iugu_customer.id)
  end

  def update_iugu_customer
    customer = Iugu::Customer.fetch(@user.iugu_customer_id)
    customer.email = @user.email
    customer.name  = @user.name
    customer.save
  end
end


Decidi criar a assinatura de forma assíncrona, pois não preciso dela imediatamente, além de garantir que uma possível falha de comunicação com o serviço externo não irá impedir o usuário de experimentar o serviço.

E olha quem mais encontramos aí: o código para criar/atualizar o Iugu::Customer. Com isso adiciono o VCR apenas aos testes que realmente precisam. Acredito que a parte de atualização do Iugu::Customer pode acabar voltando para o modelo User, sendo executada somente quando existir um iugu_customer_id, mas apenas a utilização do sistema dirá se isto será necessário.

Se tiver alguma dúvida e/ou sugestão deixa uma mensagem!