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!

quarta-feira, 10 de junho de 2015

Filme: As Vantagens de Ser Invisível

Achei um ótimo filme, depois de algum tempo descobri que existia o livro, comprei.

A adaptação para o cinema ficou muito boa, mas pra manter o chavão e ser sincero: o livro é melhor que o filme.

Repetindo, a adaptação ficou muito boa, mas um detalhe em específico me deixou muito chateado.

Em um determinado momento, Charlie (protagonista), vai defender seu amigo Patrick, que está apanhando de uns 5 caras.

No filme, parece que Charlie adquire uma força sobrenatural e nocauteia um dos brutamontes, mas no livro ele sabe bater, aprendeu com o irmão. Bata nos olhos, garganta e joelhos. Acho que isso é kung fu, não?

Um ótimo filme sobre a adolescência, seria ainda melhor sem essa apelação para o fantástico.

quarta-feira, 27 de maio de 2015

From Heroku to Dokku

I really like Heroku, it's very convenient!

Convenience normally have a price and this start to hit me. I was paying $20 for SSL endpoint and more $9 for database. Almost $30 for an application that is being reestructured and not accepting new clients, so no money!

After some googling I find this http://donpottinger.net/blog/2014/11/17/bye-bye-heroku-hello-dokku.html and decided to give it a try. I decide for the $5 plan in DigitalOcean.

I don't have any problem in deploying my application without configuring DNS, but after that I have to do so, because I use subdomains in my app. It's also good to have a hostname, because after installing Dokku you have to finish configuring it accessing the public IP address or your hostname if you already configured your DNS. As described in "Step 2: Setting up Dokku" of https://www.digitalocean.com/community/tutorials/how-to-use-the-digitalocean-dokku-application. Without this your app port will change in each deploy, so you can't have a stable URL for you app.

DigitalOcean already offer dokku 0.3.18, the latest version now, so you don't have to update it as Pottinger point out.

I have problem while compiling my assets, but after configuring DATABASE_URL to the database URL that I created there everything worked fine.

After everything configured, it's like pushing to Heroku, very cool!

Are you thinking in give DigitalOcean a try? Using the following link you win $10: https://www.digitalocean.com/?refcode=55506009d157

quinta-feira, 8 de janeiro de 2015

jQuery UI Draggable / Resizable with constrain and CSS scale transformation

There are a bunch of questions and solutions to the problems between the interaction of these technologies, these are the better solutions that I found:
  1. http://stackoverflow.com/questions/10212683/jquery-drag-resize-with-css-transform-scale
  2. https://gungfoo.wordpress.com/2013/02/15/jquery-ui-resizabledraggable-with-transform-scale-set/
  3. http://stackoverflow.com/questions/17098464/jquery-ui-draggable-css-transform-causes-jumping
Although they make an improvement, they aren't totally accurate. What they basically do is a calculation of the right position / size that the object must have after the interaction.

You can experiment with the example below, drag the small square to right. Your mouse will be outside the parent div before you hit the edge.

And now, the same example, with the solution presented in [3].

Nice, now the small square moves with the mouse, but it go beyond the parent when dragging or resizing and this was the tricky part to solve. I spend a day trying to figure out how I could solve this, so I'm sharing my solution.

First let's solve the draggable problem. The "bug" (jQuery UI guys don't want to address it, so it's not a bug) occurs because inside jQuery UI it's use absolute event positions.

Think about the parent without the scale, it will be bigger, right? So it's size to jQuery UI is beyond the limit of the scaled down version. What we need to do is inform jQuery UI that our representation is smaller.

I tried in many ways not monkey patch jQuery UI, but these efforts were fruitless. I had to expose the "contaiment" var in the ui parameter passed to callbacks. With this little modification I could use the start and stop callbacks to make jQuery UI work with my scaled containment sizes.

var dragFix, startFix, stopFix;

window.myApp = {
  layout: {
    zoomScale: 1
  },
  draggable: {
    _uiHash: function() {
      return {
        helper: this.helper,
        position: this.position,
        originalPosition: this.originalPosition,
        offset: this.positionAbs,
        containment: this.containment
      };
    }
  }
};

$.ui.draggable.prototype._uiHash = myApp.draggable._uiHash;

startFix = function(event, ui) {
  ui.containment[2] *= myApp.layout.zoomScale;
  return ui.containment[3] *= myApp.layout.zoomScale;
};

stopFix = function(event, ui) {
  ui.containment[2] /= myApp.layout.zoomScale;
  return ui.containment[3] /= myApp.layout.zoomScale;
};

dragFix = function(event, ui) {
  var deltaX, deltaY;
  deltaX = ui.position.left - ui.originalPosition.left;
  deltaY = ui.position.top - ui.originalPosition.top;
  ui.position.left = ui.originalPosition.left + deltaX / myApp.layout.zoomScale;
  return ui.position.top = ui.originalPosition.top + deltaY / myApp.layout.zoomScale;
};

Nice and clean, don't you think? After solving this, I guess that making resizable works would be easy. The inner workings of this can't be very different, right? Wrong, dead wrong. What I think will be solved in 5 minutes, take the hole day.

The resizable code is very different. I expected to see the same algorithms to apply movement constraints and other operations. This way, I had to find a new way to inform jQuery UI about my constraints.

After a day tinkering with resizable code trying to find a solution that need minimal changes to jQuery UI, like with the draggable code, I was unable to find a solution that I liked.

First, I realized that I need to change the methods e, w, n and s in _change to get correct widths and heights according to my zoom scale. This was something that could be done in the "resize" callback, but in this case it's too late in the algorithm, the scaled position is needed by internal methods, before we get a chance to change it.

After this I thought that I had ended, but resizing an element that isn't in the position 0, 0 make it grow beyond the edge of the parent. Digging a bit more, I found that I need some way to change the "woset" and "hoset" calculation, but I don't find anyway to do this without monkey patching the entire method.

The final solution is this:

Maybe you are asking yourself why I'm monkey patching. This is because I'm use Rails and I want to have the benefits of the asset pipeline.

I'm not very proud of it, so I would love to know better ways to accomplish my final result in a simple manner. If you know any, please share!

sábado, 3 de janeiro de 2015

New kid on the S3 direct upload block

Sometime ago I saw the post [Small Bites] Direct Upload para S3: a Solução Definitiva! do Akita. Ow, fantastic! I was just playing with S3 Direct Uploads solutions that are described around the web and that could save me lots of time and give me a well polished solution.

I start playing with refile and how I could integrate it with my application. It don't take much time to became disappointed.

I already use paperclip in my application, it's very well integrated, meets my requirements and I'd like how it organize files, so adapt what I have to refile would be a pain, but it opened my head that S3 Direct Upload can be simpler, so I decide to experiment.

I'd like how s3_direct_upload interact with S3, keeping the filename. This way I decide to make a hybrid of s3_direct_upload and refile: s3-upnow has born.

The idea of the gem is to be backend agnostic. For now it works only with paperclip, so if it's your upload gem, give a try to s3-upnow and your feedback. I guess that support others upload gems is not difficult.

I know that it's not well polished, but it's already working for me and maybe can work for you. If you find scenarios that it's causing problems, please let me know!

Happy hacking!