dannysmith
dannysmith

Reputation: 391

Getting 'Error cannot find parameter' when creating category with Magento SOAP API v1 and Savon Ruby Gem v2

Background

I'm trying to create a category through the Magento SOAP API using Savon. Before anyone suggests it, I can't use the Magento SOAP v2 or REST APIs.

This code sets up my client and logs in:

@client = Savon.client(wsdl: "#{EnvConfig.base_url}/api/soap/?wsdl", log_level: :debug, raise_errors: true, pretty_print_xml: true)

response = @client.call(:login, message: {
  username: EnvConfig.magento_api_user,
  apiKey: EnvConfig.magento_api_key
})

@token = response.to_hash[:login_response][:login_return]

I'm then able to call magentos various methods. To list all of the products, I can call:

@response = @client.call(:call, message: {session: @token, method: 'catalog_product.list'})

This all works properly, so there doesn't seem to be a problem with any of the code above.

The problem

When I call catalog_category.create, I'm getting an error. If I call the method with only the parentID parameter:

  @response = @client.call(:call, message: {session: @token, method: 'catalog_category.create', parent_id: 90})

then the following XML is sent by Savon:

<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:typens="urn:Magento" xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
  <env:Body>
    <typens:call>
      <session>6939ace91ba26b1da9a21334d7ef2c13</session>
      <method>catalog_category.create</method>
      <parentId>90</parentId>
    </typens:call>
  </env:Body>
</env:Envelope>

This returns the response Savon::SOAPFault: (103) Attribute "name" is required:

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
  <SOAP-ENV:Body>
    <SOAP-ENV:Fault>
      <faultcode>103</faultcode>
      <faultstring>Attribute "name" is required.</faultstring>
    </SOAP-ENV:Fault>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

I'd expect this, as the API documentation makes it clear that the following attributes are not optional:

So If I make a call containing name:

@response = @client.call(:call, message: {session: @token, method: 'catalog_category.create', parent_id: 90, category_data: {name: 'Fooooo'}})

This XML is sent:

<env:Envelope xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:typens="urn:Magento" xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
  <env:Body>
    <typens:call>
      <session>6939ace91ba26b1da9a21334d7ef2c13</session>
      <method>catalog_category.create</method>
      <parentId>90</parentId>
      <categoryData>
        <name>Fooooo</name>
      </categoryData>
    </typens:call>
  </env:Body>
</env:Envelope>

But I get a Savon::SOAPFault: (SOAP-ENV:Client) Error cannot find parameter error:

<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
  <SOAP-ENV:Body>
    <SOAP-ENV:Fault>
      <faultcode>SOAP-ENV:Client</faultcode>
      <faultstring>Error cannot find parameter</faultstring>
    </SOAP-ENV:Fault>
  </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

I've spent quite a long time trying to work this out, and have tried sending the name parameter without wrapping it in categoryData, as well as other parameters. All return the same error message.

At the very least, it would be good to know which parameter can't be found!

Any help would really be appreciated :)

Upvotes: 3

Views: 2692

Answers (1)

dannysmith
dannysmith

Reputation: 391

I could't really find a decent solution to this. In the end it was solved by building out the XML myself with this extremely messy code:

# Builds an XML Request for use with the Magento API
#-------------------------------------------------------------------------------------------------------------
# Usage:
#
# @m = MageAPI.new(token, call, params)   Creates a new MageAPIRequest object with @call, @token, @data and
#                                           @xml_data instance variables.
#
# Expects a hash of params like:
#
#      { parentId: '90',
#        categoryData: {
#          name: 'OMG, does this work?',
#          is_active: 1,
#          available_sort_by: 'position',
#          default_sort_by: 'position',
#          description: 'Foo',
#          url_key: 'url_key',
#          include_in_menu: 1}
#      }
#
#-------------------------------------------------------------------------------------------------------------
class MageAPIRequest
  attr_reader :call, :token, :data, :xml_data
  def initialize(token, call, params = {})
    @call = call
    @token = token
    @data = params
    @xml_data = build_xml
  end

  def to_s
    return @xml_data
  end

  # Build out the xml string to send to the server, excluding the Envelope, body and call tags, which are added by Savon.
  def build_xml
    token = LibXML::XML::Node.new('sessionId', @token)
    token['xsi:type']='xsd:string'
    resourcePath = LibXML::XML::Node.new('resourcePath', @call)
    resourcePath['xsi:type']='xsd:string'

    unless @data.empty?
      args = LibXML::XML::Node.new 'args'
      args['SOAP-ENC:arrayType'] = "xsd:ur-type[#{@data.length}]"
      args['xsi:type'] = 'SOAP-ENC:Array'

      # Build the structure insude 'args', based on the Hash.
      # TODO: Make this recursive, so it can work deeper than one level.
      @data.each do |k,v|
        if v.is_a? Hash

          details = LibXML::XML::Node.new(k.to_s)
          details["xsi:type"] = 'ns2:Map'
          args << details

          v.each do |key,value|

            item_node = LibXML::XML::Node.new("item")

            key_node = LibXML::XML::Node.new("key", key.to_s)
            key_node['xsi:type'] = 'xsd:string'

            if value.is_a? Fixnum
              value_node = LibXML::XML::Node.new("value", value.to_s)
              value_node['xsi:type'] = 'xsd:int'
            elsif value.is_a? Float
              value_node = LibXML::XML::Node.new("value", sprintf("%.2f", value))
              value_node['xsi:type'] = 'xsd:string'
            elsif value.is_a? Array
              value_node = LibXML::XML::Node.new("value")
              value_node['SOAP-ENC:arrayType'] = "xsd:int[#{value.length}]"
              value_node['xsi:type'] = 'SOAP-ENC:Array'

              value.each do |v|
                v_node = LibXML::XML::Node.new("item", v.to_s)
                v_node['xsi:type'] = 'xsd:string'
                v_node['xsi:type'] = 'xsd:int' if v.is_a? Fixnum
                value_node << v_node
              end

            else
              value_node = LibXML::XML::Node.new("value", value.to_s)
              value_node['xsi:type'] = 'xsd:string'
            end

            item_node << key_node
            item_node << value_node
            details << item_node

          end

        else
          pair = LibXML::XML::Node.new(k.to_s, v.to_s)
          # if v.is_a? Fixnum
          #    pair['xsi:type']='xsd:int'
          # else
            pair['xsi:type']='xsd:string'
          # end
          args << pair
        end
      end
    end
    xml_string = token.to_s
    xml_string << resourcePath.to_s
    xml_string << args.to_s
  end


end

I then used that when creating my request:

request = MageAPIRequest.new(@token, call, params)

Far from ideal, but it works. It's worth noting that I also had to build a parser to turn the responses which look like this:

  {:balance=>"10",
    :exchange_rate=>"1.000000",
    :response_type=>"ACCEPT",
    :customer_data=>
      {:item=>
        [
          {:key=>"iredeem_member_id", :value=>"110"},
          {:key=>"prefix", :value=>"Ms."},
          {:key=>"lastname", :value=>"Last name"},
          {:key=>"firstname", :value=>"First name"},
          {:key=>"iredeem_points_balance", :value=>"10"},
        ]
      }
  }

into something useable like this:

  {:balance=>"10",
    :exchange_rate=>"1.000000",
    :response_type=>"ACCEPT",
    :customer_data=>
      {:iredeem_member_id=>"110",
       :prefix=>"Ms.",
       :lastname=>"Last name",
       :firstname=>"First name",
       :iredeem_points_balance=>"10"
      }
  }

Upvotes: 0

Related Questions