How to Consume SOAP with Savon in Rails app - ruby-on-rails

I need to communicate to a service called ifthenpay via Soap using Savon on a Rails app that i'm working on.
The service generates payment references so users could pay on home banking or in cash machines.
The app needs to communicate to the service to see if the payment was made or not.
I'm using Savon and this is what i have so far in the checkout model(don't know if this is the right place to put the above code):
def self.check_status!
client = Savon.client(
wsdl: "http://www.ifthensoftware.com/IfmbWS/IfmbWS.asmx?WSDL",
endpoint: "http://localhost:3000",
namespaces: {"xmlns:ift"=>"https://www.ifthensoftware.com/"}
)
begin
response = client.call(:get_payments, message: check_status_hash)
rescue Savon::SOAPFault => error
#...
end
end
def self.check_status_hash
{
"ift:get_payments" => {
"ift:chavebackoffice" => { "ift:chavebackoffice" => "0000-0000-0000-0000" },
"ift:entidade" => {"ift:entidade" => "11202"},
"ift:subidentidade" => {"ift:subidentidade" => "202"},
"ift:dtHrInicio" => {"ift:dtHrInicio" => ""},
"ift:dtHrFim" => {"ift:dtHrFim" => ""},
"ift:referencia" => {"ift:referencia" => ""},
"ift:valor" => {"ift:valor" => ""}
}
}
end
I've an admin page where i need to list all the payments that have been made, so i can manage what was selled.
You can see the service operations here
What do i need to put in the controller and in the view for this to work?
I really appreciate your help, because i'm struggling with this for a long time.

From my point of view, and pardon me because I'm not very experienced with the use of savon, you are slightly overkilling this.
To start with, you are providing the client with a WSDL url, so what is the use of attaching a doubtfully necessary endpoint?
A namespace is, to my understanding, necessary, once again, in case there is no standard WSDl interface.
I would go, to start off, I would simply go for:
#client = Savon.client(wsdl: "http://www.ifthensoftware.com/IfmbWS/IfmbWS.asmx?WSDL")
Watch the #client instead of client. We need to assign the client to a variable that will be reachable throughout the entire process (request, process, response).
Next, you will need to prepare your request. Parsing the above url, there is a banch of methods. You are providing in your example getPayments request.
I will not use this space to tell you how to construct the hash, but the hash should look something like this:
request_hash = {
chavebackoffice: "0000-0000-0000-0000",
entidade: "11202",
subidentidade: "202",
dtHrInicio: "",
dtHrFim: "",
referencia: "",
valor: ""
}
To make the call to the api, you should simply do this:
#response = #client.call(:get_payments) do
message request_hash
end
And then, parse the #response. You will probably need to turn it to a hash first. Maybe something like this:
#data = #response.to_hash[:get_payments_response][:get_payments_result][:ifmb]
I hope this will help you enough. It should be more than enough.
Putting all up: Controller code, adapt to your need
before_action :set_client, only: [:get_payments, :other_actions_perhaps]
def get_payments
# params[:whatever] in case you post to #whatever object
# params without [:whatever] if you are using "GET" method
request_hash = {
chavebackoffice: params[:whatever][:chavebackoffice],
entidade: params[:whatever][:entidade],
subidentidade: params[:whatever][:subidentidade],
dtHrInicio: params[:whatever][:dtHrInicio],
dtHrFim: params[:whatever][:dtHrFim],
referencia: params[:whatever][:referencia],
valor: params[:whatever][:valor]
}
response = #client.call(:get_payments) do
message request_hash
end
# use this #data in your view
#data = response.to_hash[:get_payments_response][:get_payments_result][:ifmb]
end
def set_client
#client = Savon.client(wsdl: "http://www.ifthensoftware.com/IfmbWS/IfmbWS.asmx?WSDL")
end

Related

Is there a way to set the default namespace with Savon::Model?

Savon is stubborn in generating SOAP envelopes from WSDL's. It does it improperly and I see no way to fix it. It also takes the liberty of inserting the wsdl: namespace on everything for whatever reason.
The request I am building uses the tns: namespace. I'd love to be able to use Savon::Model, but right now I have to do:
client.request :tns, :function_name do
soap.body = { params }
end
Instead of something like:
super(params)
Making the request block in every function is tedious, and I have to define the function name every time instead of Savon automatically calling the correct function like what would happen in the ideal case. Right now my functions are looking like
def foo
client.request :tns, :foo do
...
end
Having to say "foo" twice seems ridiculous. Is there a way to set the default namespace for every request in a class that extends Savon::Model?
client = Savon.client do
wsdl "blah blah"
element_form_default :qualified
namespace_identifier :tem
env_namespace :soapenv
end
I am not sure if I understand your questions. I assume you are asking how to set the default namespace and wrap the request body in a function, so you don't need to write the request body every time. This code works for me, but I removed some irrelevant parts
class ExampleWS
EXAMPLE_WS_DEFAULT_NAMESPACE = "urn:example:request:1.0.0"
......
def getStockPrice( locale, stockId )
response = $client.request :get_stock_price do
soap.input = [
"ns1:getStockPrice",
{
"xmlns:ns1" => EXAMPLE_WS_DEFAULT_NAMESPACE #set default namespace here
}
]
soap.body = {
"locale" => locale,
"stockId" => stockId
}
end
end
......
end
......
# call the function
getStockPrice("en_US", 123 )
This works for me. It uses Savon 2, though:
class Soapservice
extend Savon::Model
client wsdl: "http://example.com?wsdl", env_namespace: :tns,
operations :get_resource, :put_resource
def :get_resource(id)
super(message: { id: id })
end
end
service = Soapservice.new
response = service.get_resource(1) #overwriting get_resource
# or
response = service.put_resource(message: { username: "luke", secret: "secret" })
(My example builds on the one from the official savon homepage)

confused and disoriented with paypal ipn

I am using this gem for payments in paypal https://github.com/tc/paypal_adaptive
I am very confused and disoriented with this gem. It has a poorly documented and is difficult for me to understand how to get the data from paypal on ipn response.
I hope this question will help more people having the same problem.
My steps are:
1º I send request to paypal from my orders_controller.rb with method preaproval_payment.
def preapproval_payment
preapproval_request = PaypalAdaptive::Request.new
data = {
"returnUrl" => response_paypal_user_orders_url(current_user),
"cancelUrl"=> cancel_payment_gift_url(#gift),
"requestEnvelope" => {"errorLanguage" => "en_US"},
"senderEmail" => "gift_1342711309_per#gmail.com",
"startingDate" => Time.now,
"endingDate" => Time.now + (60*60*24) * 30,
"currencyCode"=>"USD",
"maxAmountPerPayment" => "#gift.price",
"ipnNotificationUrl" => ipn_notification_url,
"ip" => request.remote_ip
}
preapproval_response = preapproval_request.preapproval(data)
puts data
if preapproval_response.success?
redirect_to preapproval_response.preapproval_paypal_payment_url
else
redirect_to gift_url(#gift), alert: t(".something_was_wrong")
end
end
2º These are the data of my request in my log console from command puts data :
{"returnUrl"=>"http://localhost:3000/en/u/maserranocaceres/orders/response_paypal", "cancelUrl"=>"http://localhost:3000/en/gifts/gift-1/cancel_payment", "requestEnvelope"=>{"errorLanguage"=>"en_US"}, "senderEmail"=>"gift_1342711309_per#gmail.com", "startingDate"=>2012-07-29 13:05:49 +0200, "endingDate"=>2012-08-28 13:05:49 +0200, "currencyCode"=>"USD", "maxAmountPerPayment"=>9, "ipnNotificationUrl"=>"http://localhost:3000/ipn_notification?locale=en", "ip"=>"127.0.0.1"}
3º I redirect to paypal page, and I make the payment on paypal successfully :D.
4º When payment is completed successfully, I am directed to:
http://localhost:3000/en/u/maserranocaceres/orders/response_paypal
I have response_paypal action in orders_controller.rb. It is GET action and my code for this action is:
def response_paypal
respond_to do |format|
format.html { redirect_to user_orders_url(current_user), :alert => "works fine return url"}
end
end
Up to this point everything works fine.
Now what I need is to get the data I received from paypal and save my database a new order if payment is successfully processed.
5º For this purpose I make a file in lib/paypal_ipn.rb and I add to this file the content from https://github.com/tc/paypal_adaptive/blob/master/templates/paypal_ipn.rb
# Allow the metal piece to run in isolation
require(File.dirname(__FILE__) + "/../../config/environment") unless defined?(Rails)
class PaypalIpn
def self.call(env)
if env["PATH_INFO"] =~ /^\/paypal_ipn/
request = Rack::Request.new(env)
params = request.params
ipn = PaypalAdaptive::IpnNotification.new
ipn.send_back(env['rack.request.form_vars'])
if ipn.verified?
#mark transaction as completed in your DB
output = "Verified."
else
output = "Not Verified."
end
[200, {"Content-Type" => "text/html"}, [output]]
else
[404, {"Content-Type" => "text/html"}, ["Not Found"]]
end
end
end
In my routes.rb I add:
match "/ipn_notification" => PaypalIpn
My 2 problems are:
a) I do not see that after making the payment this file to be fired and I can not see in my console data I get from paypal.
b) I want to send to paypal in my request, the id of object #gift for being able to recover later in paypal_ipn.rb and to save my database.
What am I doing wrong and how I can solve these problems?
Thank you
I haven't used that gem, but I've used PayPal IPN before. Here are some things you should check:
Do you have your PayPal account set up to use IPN? You must enable this setting on the account for this to work.
Have you verified that when you pass ipn_notification_url during the payment process, that it matches your "/ipn_notification" route?
For this to work, PayPal must be able to communicate directly with the server that is running this app. This means that typically, unless you have a custom setup on your local machine with dynamic DNS or something, that you will need to actually deploy this code to a server in order for PayPal to be able to communicate with your app. In other words, if this is running on http://localhost:3000, this will not work.
To answer your second question, how to recover #gift in order to record the fact it was paid in your database, I'm not entirely sure how to do it with this gem, but I'll tell you how I do it using ActiveMerchant - it is probably quite similar.
In your payment request to PayPal, you can pass in an invoice number. I believe the field is just called "invoice". Here you would pass the ID of the gift.
When PayPal notifies your app via IPN that the order was paid for, it will pass the invoice number back to you. Retrieve the #gift using this invoice number and then you can do what you need with it.
Here are the relevant parts of my working PayPal code, using the ActiveMerchant gem: https://gist.github.com/3198178
Good luck!

Problems with MailChimp API in Ruby Error Code: -90

I am using the following code in my MailChimp Controller to submit simple newsletter data. When It is sent I receive the following error as a "Method is not exported by this server -90" I have attached my controller code below. I am using this controller for a simple newsletter signup form. (Name, Email)
class MailchimpController < ApplicationController
require "net/http"
require "uri"
def subscribe
if request.post?
mailchimp = {}
mailchimp['apikey'] = 'f72328d1de9cc76092casdfsd425e467b6641-us2'
mailchimp['id'] = '8037342dd1874'
mailchimp['email_address'] = "email#gmail.com"
mailchimp['merge_vars[FNAME]'] = "FirstName"
mailchimp['output'] = 'json'
uri = URI.parse("http://us2.api.mailchimp.com/1.3/?method=listSubscribe")
response = Net::HTTP.post_form(uri, mailchimp)
mailchimp = ActiveSupport::JSON.decode(response.body)
if mailchimp['error']
render :text => mailchimp['error'] + "code:" + mailchimp['code'].to_s
elsif mailchimp == 'true'
render :text => 'ok'
else
render :text => 'error'
end
end
end
end
I highly recommend the Hominid gem: https://github.com/tatemae-consultancy/hominid
The problem is that Net::HTTP.post_form is not passing the "method" GET parameter. Not being a big ruby user, I'm not certain what the actual proper way to do that with Net::HTTP is, but this works:
require "net/http"
data="apikey=blahblahblah"
response = nil
Net::HTTP.start('us2.api.mailchimp.com', 80) {|http|
response = http.post('/1.3/?method=lists', data)
}
p response.body
That's the lists() method (for simplicity) and you'd have to build up (and urlencode your values!) your the full POST params rather than simply providing the hash.
Did you take a look at the many gems already available for ruby?
http://apidocs.mailchimp.com/downloads/#ruby
The bigger problem, and main reason I'm replying to this, is that your API Key is not obfuscated nearly well enough. Granted I'm used to working with them, but I was able to guess it very quickly. I would suggest immediately going and disabling that key in your account and then editing the post to actually have completely bogus data rather than anything close to the correct key. The list id on the other hand, doesn't matter at all.
You'll be able to use your hash if you convert it to json before passing it to Net::HTTP. The combined code would look something like:
mailchimp = {}
mailchimp['apikey'] = 'APIKEYAPIKEYAPIKEYAPIKEY'
mailchimp['id'] = '8037342dd1874'
mailchimp['email_address'] = "email#gmail.com"
mailchimp['merge_vars[FNAME]'] = "FirstName"
mailchimp['output'] = 'json'
response = nil
Net::HTTP.start('us2.api.mailchimp.com', 80) {|http|
response = http.post('/1.3/?method=listSubscribe', mailchimp.to_json)
}

RSpec Google Contacts Connection

I'm trying to test out a controller action on Rails 2.3.10 that connect to Google to retrieve contacts. I'm using Rspec and Mocha for testing. I want to stub out the actual call to Google since this is a unit test. I want to verify that the authsub_url method is called with the correct parameters. Stubbing the method out causes the expectation to fail.
Any advice would be appreciated.
Thanks!
My method that sets up the client to google is below:
def setup_client
#client = GData::Client::DocList.new(:authsub_scope => CONTACTS_SCOPE, :source => 'google-DocListManager-v1.1', :version => '3.0')
if params[:token].nil? && session[:google_token].nil?
#authsub_link = #client.authsub_url(import_method_gmail_url, false, true)
render :action => :index, :layout => "empty"
elsif params[:token] && session[:google_token].nil?
#client.authsub_token = params[:token]
session[:google_token] = #client.auth_handler.upgrade
end
#client.authsub_token = session[:google_token] if session[:google_token]
end
Here is my test:
describe "setup_client" do
it "has a authsub_link if there is no token parameter and the google token is not present in the session" do
GData::Client::DocList.any_instance.stubs(:authsub_url).returns("http://test.google.com/contacts")
user = Factory(:subscriber_user)
profile = Factory(:profile, :user => user)
login_as user
controller.instance_variable_get(:#client).expects(:authsub_url).with(import_method_gmail_url, false, true).once
get :index
assigns(:authsub_link).should == "http://test.google.com/contacts"
end
end
I would recommend FakeWeb. It allows you to fake web requests. Simple define the URL you're going to call and prepare the response(s). Makes your life very easy.
It looks like you are stubbing out the DocList#authsub_url method in two places :-
The first stub is on any instance of DocList and returns a URL :-
GData::Client::DocList.any_instance.stubs(:authsub_url).returns("http://test.google.com/contacts")
The second stub is on the actual instance of DocList but this returns nil because no there is no returns clause :-
controller.instance_variable_get(:#client).expects(:authsub_url).with(import_method_gmail_url, false, true).once
I think you can achieve what you want by combining them something like this :-
controller.instance_variable_get(:#client).expects(:authsub_url).with(import_method_gmail_url, false, true).returns("http://test.google.com/contacts")
Note that once is the default so is not needed unless you want to emphasise that the method should only be called once.

SimpleDB to ActiveResource. Rails

Im looking for a way to map an ActiveResource to SimpleDB
I want to avoid plugins/gems as all I have used are outdated/buggy/not mantained
It doesnt seem hard, I wonder if any of you have succesfully implemented a rails app with simpleDB as an Active Resource. How did you do it? Thanks.
I haven't worked with SimpleDB, but I have mapped ActiveResource to Amazon's Flexible Payments Service REST api and just skimming the docs they seem similar so here's basically what I did, maybe you can use this as a starting point.
require 'base64'
require 'openssl'
class AmazonFlexiblePaymentResource < ActiveResource::Base
self.site = AMZ_CONFIG['flexible_api_url']
def self.rest_api(options = {})
params = common_request_params.update(options)
sig = compute_signature(AMZ_CONFIG['secret_access_key'], 'get', site, params)
rest_req = {'Signature' => sig}.update(params)
# make the http get call
connection.get("/#{query_string(rest_req)}", headers)
end
protected
# these are the params are common to all rest api calls
def self.common_request_params
{ 'AWSAccessKeyId' => AMZ_CONFIG['access_key_id'],
'SignatureVersion' => 2,
'SignatureMethod' => 'HmacSHA256',
'Timestamp' => Time.now.utc.iso8601,
'Version' => '2008-09-17'}
end
def self.compute_signature(key, method, end_point_url, params)
query_str = parameters.sort.collect {|k, v| v.to_query(k)}.join '&'
# cannot use plus for space, and tilde needs to be reversed
query_str.gsub!('+', '%20')
query_str.gsub!('%7E', '~')
to_sign = [method.upcase, end_point_uri.host.downcase,
end_point_uri.request_uri, query_str].join "\n"
digest = OpenSSL::Digest::Digest.new('sha256')
hmac = OpenSSL::HMAC.digest(digest, key, to_sign)
Base64.encode64(hmac).chomp
end
end
Then I just make calls like this
res = AmazonFlexiblePaymentResource.rest_api({ 'Action' => 'GetTransactionStatus', 'TransactionId' => '1234567890ABCDEFGHIJ' })
And the response is a hash of the parsed xml. Again this works for Amazon Flexible Payments Service, so you may need to make adjustments to match the SimpleDB REST API.
Does it need to be ActiveResource? If you want it like ActiveRecord, check out SimpleRecord.
http://github.com/appoxy/simple_record
It's very actively maintained.

Resources