viernes, 18 de marzo de 2011

Integración con Moneybookers con formulario HTML

¿Quieres integrar una pasarela de pagos en tu aplicación rails? ¿Prefieres integrar con moneybookers en lugar de la típica pasarela de paypal? Pues hay dos formas: con un formulario html que describe moneybookers o con una gema para realizar el submit del formulario desde nuestro servidor rails. Ambas soluciones utilizan activemerchant.

Antes de empezar a tirar código, date de alta en moneybookers como "merchant", es decir, como vendedor. Créate también una cuenta como comprador. Luego les mandas un mail y les dices que hagan que esas cuentas sean de prueba, para que puedas empezar a integrar. Si necesitas pagos recurrentes, también tendrás que mandarles un mail. Si sólo vas a utilizar unos determinados tipos de pago, tendrás que enviarles un mail para que te habiliten el fixed split gateway... En resumen, todo por mail... y un poco lento.

Parece ser que no permiten pagos recurrentes para usuarios que no tengan cuenta de comprador en moneybookers: FAIL.

Moneybookers describe en su manual y en su página de pruebas, el contenido del formulario que se debe enviar a su servidor. Así mismo, activemerchant tiene un módulo de "integraciones" que permite generar formularios de este tipo para varias pasarelas.

En cuanto a los parámetros, hay que destacar los siguientes:
  • return_url_target y cancel_url_target permiten pasar el nombre de un iframe para incrustar el formulario de moneybookers en tu aplicación.
  • Una vez realizado el pago, moneybookers llama a la dirección especificada en notify_url para que le indiquemos si el pago ha sido validado por nosotros.
  • hide_login permite ocultar el formulario de login en moneybookers

Formulario HTML:

# app/helpers/payment_helper.rb
module PaymentsHelper
  include ActiveMerchant::Billing::Integrations::ActionViewHelper

end

# app/views/payments/new.html.erb

<iframe id="mb-iframe"
        name="mb-iframe"
        frameborder="no"
        style="height: 600px; width:500px;"
        scrolling = "auto">
</iframe>

<% payment_service_for @payment.transaction_id, "paytoemail@merchant.com",
                                                :amount => @payment.amount,
                                                :currency => 'EUR',
                                                :service => :moneybookers,
                                                :html => { :id => 'mb-form',
                                                           :target => 'mb-iframe' } do |service| %>

  <% service.customer :first_name => @payment.owner.first_name,
                      :last_name  => @payment.owner.last_name,
                      :email      => @payment.owner.email %>

  <% service.billing_address :city     => @payment.owner.city,
                             :address1 => @payment.owner.street1,
                             :address2 => @payment.owner.street2,
                             :zip      => @account.owner.postal_code,
                             :country  => @account.owner.country.code %>

  <% service.notify_url "http://localhost:3000/payments/notify" %>
  <% service.return_url url_for(:only_path => false, :action => 'done', :merchant_id => "2200220022") %>
  <% service.cancel_return_url "http://localhost:3000/payments" %>

  <%= hidden_field_tag :return_url_target,     "mb-iframe" %>
  <%= hidden_field_tag :cancel_url_target,     "mb-iframe" %>
  <%= hidden_field_tag :language,              "ES" %>
  <%= hidden_field_tag :hide_login,            "1" %>
  <%= hidden_field_tag :detail1_description,   "Bach Complete Collection" %>
  <%= hidden_field_tag :detail1_text,          "#{@payment.amount} €" %>


  <%= submit_tag "Pagar" %>

<% end %>


# app/controllers/payment_controller.rb 
def notify
  http_status = 500
  notify = ActiveMerchant::Billing::Integrations::Moneybookers::Notification.new(request.raw_post)


  if notify.acknowledge(CONFIG['moneybookers']['secret'])
    begin
      if notify.complete?
        payment = Payment.find_by_transaction_id(notify.transaction_id)

        if notify.posted_amount.to_f == payment.amount.to_f

          payment.status = notify.status
          payment.save

          http_status = 200
        else
          logger.error("Failed to verify MoneyBookers's notification parameters, please investigate")
        end
      else

        logger.error("Failed to verify MoneyBookers's notification, please investigate")
      end
    rescue Exception => ex
      logger.error(ex.message)
    end
  else
    logger.error("Failed to verify MoneyBookers's notification, please investigate")
  end

  render :nothing => true, :status => http_status
end



Lo malo de esta opción es que el formulario está visible en el navegador del cliente, y se podría modificar el contenido. La mejor opción es utilizar el parámetro "prepare_only", que permite enviar toda esta información desde el servidor rails, y moneybookers nos contesta con un identificador de sesión en su site, así que ya puedes entrar en https://www.moneybookers.com/app/payment.pl?sid=<id de sesion> y ahí ya está toda la información necesaria. Así el navegador no tiene ningún tipo de información "sensible". Este método lo describiré en el siguiente post.

martes, 30 de noviembre de 2010

Tablas particionadas

Si tienes una tabla con varios millones de registros y sabes de antemano que se va a consultar por periodos mensuales, puedes particionar la tabla por años y meses. Así cuando hagas consultas, sólo utilizarás la partición correspondiente y mejorará el rendimiento.

Al particionar por los valores de un determinado campo, es obligatorio tener dicho campo como parte de la primary key de la tabla.

A continuación una migración para crear una tabla particionada por meses y años:

 class CreateHistoryAlerts < ActiveRecord::Migration

  FIRST_YEAR = 2010
  LAST_YEAR  = 2020

  def self.up
    create_table :history_alerts do |t|
      t.integer :agent_id,              :references => nil
      t.integer :alert_type_id,         :references => nil
      t.integer :country_id,            :references => nil
      t.string  :reason
      t.boolean :read

      t.timestamps
    end

    sql = "ALTER TABLE `history_alerts` drop primary key, add primary key(id, created_at)"
    execute(sql);


    sql = "ALTER TABLE `history_alerts` PARTITION BY RANGE (YEAR(created_at)) \
           SUBPARTITION BY HASH (MONTH(created_at)) \
           ("

    for year in (FIRST_YEAR..LAST_YEAR)
      sql += "PARTITION p_#{year} VALUES LESS THAN (#{year}) \
              (SUBPARTITION s_jan_#{year}, \
               SUBPARTITION s_feb_#{year}, \
               SUBPARTITION s_mar_#{year}, \
               SUBPARTITION s_apr_#{year}, \
               SUBPARTITION s_may_#{year}, \
               SUBPARTITION s_jun_#{year}, \
               SUBPARTITION s_jul_#{year}, \
               SUBPARTITION s_aug_#{year}, \
               SUBPARTITION s_sep_#{year}, \
               SUBPARTITION s_oct_#{year}, \
               SUBPARTITION s_nov_#{year}, \
               SUBPARTITION s_dec_#{year}),"
    end
    sql += "PARTITION p_all VALUES LESS THAN MAXVALUE \
            (SUBPARTITION s_jan_all, \
             SUBPARTITION s_feb_all, \
             SUBPARTITION s_mar_all, \
             SUBPARTITION s_apr_all, \
             SUBPARTITION s_may_all, \
             SUBPARTITION s_jun_all, \
             SUBPARTITION s_jul_all, \
             SUBPARTITION s_aug_all, \
             SUBPARTITION s_sep_all, \
             SUBPARTITION s_oct_all, \
             SUBPARTITION s_nov_all, \
             SUBPARTITION s_dec_all))"
    execute(sql);
  end


  def self.down
    drop_table :history_alerts
  end

end

lunes, 7 de junio de 2010

Tabs On Rails

Ya estaba cansado de ver toda la morralla de código en las vistas para pintar las pestañas, y al final he probado el plugin tabs_on_rails (http://code.simonecarletti.com/projects/tabsonrails/wiki)

Mucho más limpio y sencillo. En las vistas solemos tener un partial que pinta las pestañas. Aquí podemos poner algo como:

<% tabs_tag(:builder => MyTabBuilder) do |tab| %>
  <% if logged_in? %>
    <%= tab.home I18n.t('tabs.home'), member_dashboard_path %>
    <%= tab.members I18n.t('tabs.members'), member_profiles_path %>
  <% else %>
    <%= tab.home I18n.t('tabs.home'), root_path %>
    <%= tab.members I18n.t('tabs.members'), profiles_path %>
  <% end %>

<% end %>

Si quieres personalizar la forma de pintar las pestañas, puedes crear tu propio Builder en el directorio lib, por ejemplo:

class MyTabBuilder < TabsOnRails::Tabs::Builder
  def tab_for(tab, name, options)
    cls = (current_tab?(tab) ? 'selected' : '')
    @context.content_tag(:li) do
      @context.link_to(name, options, :class => cls)
    end
  end
end


Ahora en los controladores, eliges la pestaña que quieres seleccionar, pudiendo incluir lógica o diferenciar por action:

class ProfilesController < ApplicationController
  set_tab :home
  set_tab :members, :if => :my_profile?
  set_tab :posts, :only => %w(list edit)
 
  ...
  private

  def my_profile?
    !params[:id].blank? && params[:id].to_i == current_user.id
  end

end

jueves, 11 de febrero de 2010

merge_conditions

Más de una vez hemos tenido que crear una query dinámicamente, en función de los parámetros que recibimos.

Una opcion es mantener el string de la condicion y una hash con los parametros:

str_conditions => "email is not null"
h_conditions = {}

if !params[:group_ids].blank?
  str_conditions += " and group_id in (?)"
  h_conditions[:group_ids] = params[:groups_ids]
end

if !params[:manager_id].blank?
  str_conditions += " and manager_id = ?"
  h_conditions[:manager_id] = params[:manager_id]
end

User.find :all, :conditions => [str_conditions, h_conditions]


Pero se engorrina bastante, con tanto "+=" y con tanto "and" y con tanta hash.

Otra opcion más clara es tener cada grupo de condiciones separado, y utilizar luego el metodo merge_conditions de ActiveRecord::Base.

c1=(params[:group_ids].blank?) ? [] : ["group_id in (?)", params[:groups_ids]]
c2=(params[:manager_id].blank?) ? [] : ["manager_id = ?", params[:manager_id]]


User.find :all, :conditions => merge_conditions("email is not null", c1, c2)

martes, 26 de enero de 2010

Configuracion de las aplicaciones

Para definir los parámetros configurables de la aplicación, utilizo lo siguiente: un fichero config/config.yml y un initializer config/initializers/local_config.rb:

En el config.yml, mantengo en formato yaml los valores de los parámetros:

development:
  authentication: "restful" 
  manager:
    activities:
      activate: "Activate"
      create: "Create"

En local_config.rb:

APP_CONFIG = YAML.load_file("#{RAILS_ROOT}/config/config.yml")[RAILS_ENV]


Así, desde cualquier punto de la aplicación puedo hacer:

if APP_CONFIG['authentication'] == 'restful'
...
end

o

lst = APP_CONFIG['manager']['activities']

que devolveria una hash con:

{"create"=>"Create", "activate"=>"Activate"}

lunes, 25 de enero de 2010

Formularios personalizados

Tienes que pintar siempre el mismo tipo de formulario en la aplicacion? Por ejemplo, tienes que pintar siempre una etiqueta al lado del input?

Tienes un formulario del siguiente estilo:

<% form_for :user do |f| %>
<fieldset>
    <p>
      <label>Login</label><br/>
      <%= f.text_field :login %>
   </p>
    <p>
      <label>Password</label><br/>
      <%= f.password_field :password %>
   </p>
</fieldset>
<% end %>


Quedaría más limpio así:

<% form_for :user, :builder=>LabeledFormBuilder do |f| %>
<fieldset>
     <%= f.text_field :login, :label=>"Login" %>
     <%= f.password_field :password, :label=>"Password" %>
</fieldset>
<% end %>


Todo lo que tienes que hacer es crearte una clase en app/builders:

class LabeledFormBuilder < ActionView::Helpers::FormBuilder

  helpers = field_helpers +
            %w{date_select datetime_select time_select} +
            %w{collection_select select country_select time_zone_select} -
            %w{hidden_field label fields_for} # Don't decorate these

  helpers.each do |name|
    define_method(name) do |field, *args|
      options = args.last.is_a?(Hash) ? args.pop : {}
      label = label(field, options[:label], :class => options[:label_class])
      @template.content_tag(:p, label +'
' + super)  #wrap with a paragraph
    end
  end

end



y añadir a tu config/environment.rb:


config.load_paths += %W( #{RAILS_ROOT}/app/builders)


Si quieres que por defecto todos tus formularios usen este builder, inicializa en application_helper o en algun initializer:

ActionView::Base.default_form_builder = LabeledFormBuilder

Controllers y Presenters

Cuando nos queremos dar cuenta, tenemos los controladores llenos de código y empiezan a ser pesados y difíciles de mantener. He visto que hay un patrón de diseño que permite mantener toda esta lógica "sobrante" en otra clase, y dejar en los controladores sólo la funcionalidad de recuperar datos de los modelos y decidir el flujo de la navegación. A este patrón lo llaman Presenter.

Consiste en crear una clase donde se haga toda la recogida de datos que se hace en los controladores. Por ejemplo, antes tenia un controlador con esta acción:

def show
  @profile = Profile.active.find(params[:id])
  @friends = @profile.friends.first 12
  @recommended_profiles = @profile.recommended(:limit => 12)
  @graffities = @profile.graffities.all(:limit => 12)
  Comment.preload_wall_associations(@graffities)
 

  store_location
 

  respond_to do |format|
    format.html # show.html.erb
    format.xml { render :xml => @profile }
  end
end


Los hay peores, pero esto se puede sustituir por:

  def show
    @presenter = ProfilePresenter.new(params)
    store_location

    respond_to do |format|
      format.html # index.html.erb
      format.xml { render :xml => @presenter }
    end
  end


Como se ve, en ProfilePresenter dejamos toda la lógica de recogida de datos.

class ProfilePresenter

  def initialize(params)
    @presenter_params = params
  end
 
  def profile
    @profile ||= Profile.active.find(@presenter_params[:id])
  end

  def friends
...

  end


  def recommended
...
  end


  def graffities
...
  end

end



Asi, en las vistas, podemos utilizar @presenter.profile o @presenter.friends, etc. y dejamos los controladores claros y limpios.

Estos presenters pueden dejarse en el directorio app/presenters, por ejemplo. No te olvides de añadir a config/environment.rb la linea:

config.load_paths += %W( #{RAILS_ROOT}/app/presenters)