Spree Commerce, eBay Trading API, and the eBay Accelerator Toolkit from Intradesys EbatNs

I thought that I would write this out to help future users, I may turn this into a plugin/extension for Spree at some point, but for now, I will just give you the basic files and ideas. Please note, now and again when I save this file, some weird code errors show up. Please correct an obvious errors without notifying me, because if it’s truly glaring, it’s probably something that happened while editing in WordPress. Like str_replace(“<!–$bt–>”, is obviously wrong, but it keeps doing it, go figure.

I have some custom columns on the Taxons and Products tables;

 mysql> describe products;
+-----------------------+--------------+------+-----+---------+----------------+
| Field                 | Type         | Null | Key | Default | Extra          |
+-----------------------+--------------+------+-----+---------+----------------+
| id                    | int(11)      | NO   | PRI | NULL    | auto_increment |
| name                  | varchar(255) | NO   | MUL |         |                |
| description           | text         | YES  |     | NULL    |                |
...
| send_to_ebay          | tinyint(4)   | NO   |     | 0       |                |
| ebay_revise           | tinyint(4)   | NO   |     | 0       |                |
| was_on_ebay           | tinyint(4)   | NO   |     | 0       |                |
| is_on_ebay            | tinyint(4)   | NO   |     | 0       |                |
| ebay_id               | bigint(20)   | NO   |     | NULL    |                |
+-----------------------+--------------+------+-----+---------+----------------+
mysql> describe taxons;

+---------------------+--------------+------+-----+---------+----------------+
| Field               | Type         | Null | Key | Default | Extra          |
+---------------------+--------------+------+-----+---------+----------------+
| id                  | int(11)      | NO   | PRI | NULL    | auto_increment |
| taxonomy_id         | int(11)      | NO   | MUL | NULL    |                |
| parent_id           | int(11)      | YES  | MUL | NULL    |                |
| position            | int(11)      | YES  |     | 0       |                |
...
| ebay_category       | varchar(255) | YES  |     | NULL    |                |
| ebay_store_category | varchar(255) | YES  |     | NULL    |                |
+---------------------+--------------+------+-----+---------+----------------+
mysql> describe ebay_api;

+---------------------+--------------+------+-----+---------+----------------+
| Field               | Type         | Null | Key | Default | Extra          |
+---------------------+--------------+------+-----+---------+----------------+
| call_name       | varchar(255) | YES  |     | NULL    |                |
| errors | tinyint(4) | YES  |     | NULL    |                |
| times_called | int(11) | YES  |     | NULL    |                |
| was_notified | tinyint(4) | YES  |     | NULL    |                |
+---------------------+--------------+------+-----+---------+----------------+

Your tables don’t have to look like that, and there is nothing to stop you from getting and storing the information
any other way. I was working with a legacy table and database from the clients eBay FileExchange .csv days. The last
table is for some basic reporting, it tracks the errors on the API calls and if there are more than 3 errors, it notifies me
by email and stops sending until it is reset.

Once your tables look like that, you will need to have a directory structure/files in your RAILS_ROOT like so:

|-- lib
|   |-- AddItem.php
|   |-- config <-- Copy the dir from the EbatsNs download, and create the file ebay.token
|   |   |-- ebay.token
|   |   |-- ebay.config.php
|   |   `-- ebay.config.sandbox.php
|   |-- EbatNs <-- Download this from InTradeSys, Ebay Accelerator Toolkit New Schema for PHP5
|   |   |-- AbstractRequestType.php
|   |   |-- AbstractResponseType.php
|   |   |-- ...
|   |   `-- XSLFileType.php
[You will need to touch the rest of these files, just touch, don't put anything in them yet]
|   |-- ebay.config.php
|   |-- ebay_end.php
|   |-- ebay_send.php
|   |-- EndItem.php
|   |-- EbayHelper.php
|   |-- ReviseItem.php
|   |-- set_ebay_notify.php
|   `-- setincludepath.php
|-- public
|   |-- 404.html
|   |-- 500.html
|   |--  ...
|   |-- ebay_notify.php <-- Touch this file too

We put everything in lib, except ebay_notify.php which is the notification handler. Now let's create the files:

//lib/setincludepath.php
function __autoload($class) {
  $ebats = "/var/www/yourrailsroot/lib/EbatNs/$class.php";
  $norm = "/var/www/yourrailsroot/lib/$class.php";
  if (file_exists($ebats)) {
    include $ebats;
  } else if (file_exists($norm)) {
    include $norm;
  }
}
//lib/AddItem.php
  class AddItem extends EbatNs_Environment {
    public $res;
    public $resultBody = '';
    public $pIds = array();
    public $sIds = array();
    public function dispatchCall($product,$desc,$category) {
      $picURL = "http://yourimageurl";
      $req = new AddItemRequestType();
      $item = new ItemType();
      //$item->BuyItNowPrice
      $item->Description = $desc;
      $item->ListingDuration = 'GTC';
      if (strlen($product->name) > 55) {
        $product->name = substr($product->name,0,54);
      }
      $item->Title = $product->name;
      $item->Currency = 'GBP';
 // We are only doing fixed price, its similar for AddItemRequest
      $item->ListingType = 'FixedPriceItem';
      $item->Quantity = 1;
      $item->StartPrice = new AmountType();
      $item->StartPrice->setTypeValue($product->price);
      $item->StartPrice->setTypeAttribute('currencyID', 'GBP');
      $item->Country = 'GB';
      $item->Location = '-- not given --';
      $item->SKU = $product->image_number;
// This is an internal SKU, use any one you want,
// if you have alot of variants, you might have to finaigle this value
      $item->DispatchTimeMax = 3;

      $item->Storefront = new StorefrontType();
      $item->Storefront->StoreCategoryID = $category->ebay_store_category;

      $item->PrimaryCategory = new CategoryType();
      $item->PrimaryCategory->CategoryID = $category->ebay_category;

      $item->Site = 'UK';

      $item->PaymentMethods[] = 'PayPal';
      $item->PayPalEmailAddress = 'yourpayalemail';

      $item->PictureDetails = new PictureDetailsType();
      $item->PictureDetails->PictureURL = "$picURL/" . $product->image_number . ".jpg";

      $item->ShipToLocations[] = "GB";
      $item->ShipToLocations[] = "Europe";
      $item->ShipToLocations[] = "Worldwide";

      $localShippingOptions = array(
          'UK_RoyalMailFirstClassStandard' => 0.5,
          'UK_RoyalMailSpecialDeliveryNextDay' => 5
      );
      $p = 1;
      foreach ($localShippingOptions as $k=>$v) {
          $ShippingServiceOptions = new ShippingServiceOptionsType();
          $cost = new AmountType();
          $cost->setTypeValue($v);
          $cost->setTypeAttribute('currencyID', 'GBP');
          $ShippingServiceOptions->setShippingService($k);
          $ShippingServiceOptions->setShippingServiceCost($v);
          $ShippingServiceOptions->setShippingServicePriority($p);
          $shipping[]=$ShippingServiceOptions;
          $p++;
      }
      $intlShipping = array(
            'UK_RoyalMailAirmailInternational' => array (
                'Europe' => 1,
                'Worldwide' => 1.50
                ),
          'UK_RoyalMailInternationalSignedFor' => array (
                'Europe' => 5,
            )
        );
      $pr = 1;
      foreach ($intlShipping as $method=>$arr) {
        foreach ($arr as $loc=>$price) {
            $InternationalShippingServiceOptions = new InternationalShippingServiceOptionsType();
            $cost = new AmountType();
            $cost->setTypeValue($price);
            $cost->setTypeAttribute('currencyID', 'GBP');
            $InternationalShippingServiceOptions->setShippingService($method);
            $InternationalShippingServiceOptions->setShippingServiceCost($price);
            $InternationalShippingServiceOptions->setShippingServicePriority($pr);
            $InternationalShippingServiceOptions->setShipToLocation($loc);
            $shippingInternational[]=$InternationalShippingServiceOptions;
            $pr++;
        }
      }

      $shippingObj = new ShippingDetailsType();
      $shippingObj->ShippingType='Flat';
      $shippingObj->setShippingServiceOptions($shipping, null);
      $shippingObj->setInternationalShippingServiceOption($shippingInternational,null);
      $item->setShippingDetails($shippingObj,null);
      $req->setItem($item);
      echo "Adding {$product->image_number}...";
      $this->res = $this->proxy->AddItem($req);
      echo "Add Complete {$product->image_number}\n";
      if ($this->testValid($this->res) || $this->res->getAck() == 'Warning') {
            mysql_query("update products set send_to_ebay = 0,
ebay_id = '{$this->res->ItemID}',
 was_on_ebay = '1', is_on_ebay = '1' where id='{$product->id}'");
            $this->resultBody .= "Item: {$product->image_number} sent to ebay, ItemID is {$this->res->ItemID}\n";
            $this->sIds[] = $product->id;
      } else {
         // THis is where you increment the ebay_api.errors field, do it for all actions
        $this->resultBody .= "Item: {$product->image_number} ". $this->res->getAck() ."\n";
          foreach ($this->res->Errors as $error) {
            $this->resultBody .= "\tSeverity: {$error->SeverityCode} - " . $error->LongMessage . "\n";
          }
      }

    } //end dispatchCall

    public function sendEmail() {
      $sql = "update products set send_to_ebay = '1', is_on_ebay = 0, was_on_ebay = 0 where id in (".join(',',$this->sIds).");";
      $this->resultBody .= "Undo SQL: $sql\n";
      mail("youremail","Ebay API Send Complete", $this->resultBody);
    }
  }
//lib/ReviseItem.php
class ReviseItem extends EbatNs_Environment {
    public $res;
    public $resultBody = '';
    public $pIds = array();
    public $sIds = array();
    public function dispatchCall($product,$desc,$category) {
      $req = new ReviseFixedPriceItemRequestType();
      $item = new ItemType();
      //$item->BuyItNowPrice
      $item->Description = $desc;
      if (strlen($product->name) > 55) {
        $product->name = substr($product->name,0,54);
      }
      $item->ItemID = $product->ebay_id;
      //$item->SKU = $product->image_number;
      $item->Title = $product->name;
      $item->Description = $desc;
      $item->BuyItNowPrice = $product->price;
      $item->Storefront = new StorefrontType();
      $item->Storefront->StoreCategoryID = $category->ebay_store_category;

      $item->PrimaryCategory = new CategoryType();
      $item->PrimaryCategory->CategoryID = $category->ebay_category;

      $req->setItem($item);
      echo "Revising {$product->image_number}...";
      $this->res = $this->proxy->ReviseFixedPriceItem($req);
      echo "Reivise Complete\n";
      if ($this->testValid($this->res) || $this->res->getAck() == 'Warning') {
            mysql_query("update products set send_to_ebay = 0, ebay_revise = 0, is_on_ebay = '1' where id='{$product->id}'");
            $this->resultBody .= "Item: {$product->image_number} revised to ebay, ItemID is {$this->res->ItemID}\n";
            $this->sIds[] = $product->id;
      } else {
        $this->resultBody .= "ItemRevise: {$product->image_number} ". $this->res->getAck() ."\n";
          foreach ($this->res->Errors as $error) {
            $this->resultBody .= "\tSeverity: {$error->SeverityCode} - " . $error->LongMessage . "\n";
            /* On Occaision items don't get flagged as ended on ebay, so catch this error */
            if (eregi('not allowed to revise ended auctions',$error->LongMessage)) {
              mysql_query("update products set send_to_ebay = 0, ebay_revise = 0, is_on_ebay = '1' where id='{$product->id}'");
            }
          }
      }

    } //end dispatchCall

    public function sendEmail() {
      $sql = "update products set ebay_revise = '1' where id in (".join(',',$this->sIds).");";
      $this->resultBody .= "Undo SQL: $sql\n";
      mail("youremail","Ebay API Revise Complete", $this->resultBody);
    }
  }
//lib/EndItem.php
class EndItem extends EbatNs_Environment{
  public $res;
  public $resultBody = '';
  public function dispatchCall ($product,$reason='NotAvailable'){
      $req = new EndFixedPriceItemRequestType();
      if (!empty($product->ebay_id)) {
        $req->setItemID($product->ebay_id);
      }
      $req->setSKU($product->image_number);
      $req->setEndingReason($reason);

      $this->res = $this->proxy->EndFixedPriceItem($req);
      if ($this->testValid($this->res) || $this->res->getAck() == 'Warning'){
          mysql_query("update products set is_on_ebay = '0' where id = '{$product->id}'");
          $this->resultBody .= "Item: {$product->image_number} ended on ebay\n";
      } else {
        $this->resultBody .= "EndingItem: {$product->image_number} ". $this->res->getAck() ."\n";
        foreach ($this->res->Errors as $error) {
          $this->resultBody .= "\tSeverity: {$error->SeverityCode} - " . $error->LongMessage . "\n";
        }
      }
  }
  public function sendEmail() {
    mail("youremail","Ebay API EndItem Complete", $this->resultBody);
  }
}
lib/EbayHelper.php
class EbayHelper extends EbatNs_Environment {
	public function get_notification_preferences ($prefLevel){
    $req = new GetNotificationPreferencesRequestType();
    $req->setPreferenceLevel($prefLevel);

    return $this->proxy->GetNotificationPreferences($req);
  }
  public function get_notification_usage() {
    $req = new GetNotificationsUsageRequestType();
    return $this->proxy->GetNotificationsUsage($req);
  }
}
//lib/ebay_send.php
include "setincludepath.php";
	$item = new AddItem();
	$item->resultBody .= 'Ebay Send started at: ' . date("Y-m-d H:i:s") . "\n--------------\n";
	$connection = mysql_connect( ... );
	mysql_select_db('yourapp_production');
   
        // Look at the ebay_api errors for this call, and see if they are greater than 3, if so, just die out and notify by email

	$res = mysql_query("select p.*,v.price from products p, variants v where p.send_to_ebay = '1' and v.product_id = p.id limit 100;");
	while ($product = mysql_fetch_object($res)) {
	  $pIds[] = $product->id;
	  $cres = mysql_query("select t.* from
	                              taxons t,products_taxons pt where
	                              t.id = pt.taxon_id and
	                              pt.product_id = '{$product->id}';");
	  $category = mysql_fetch_object($cres);

    $desc = $product->description;

    $item->dispatchCall($product,$desc,$category);
	  //mysql_query("update products set send_to_ebay = '0' where id = '{$product->id}'");
	}
	if (mysql_num_rows($res)) {
	  echo "Sending Email\n";
	  $item->sendEmail();
	}

	// Item revise code

	$item = new ReviseItem();
	$item->resultBody .= 'Ebay Revise started at: ' . date("Y-m-d H:i:s") . "\n--------------\n";
	$res = mysql_query("select p.*,v.price from products p, variants v where p.ebay_revise = '1' and v.product_id = p.id limit 100;");
	while ($product = mysql_fetch_object($res)) {
	  $pIds[] = $product->id;
	  $cres = mysql_query("select t.* from
	                              taxons t,products_taxons pt where
	                              t.id = pt.taxon_id and
	                              pt.product_id = '{$product->id}';");
	  $category = mysql_fetch_object($cres);

    $desc = $product->description; // We have a custom template code here... :) 

    $item->dispatchCall($product,$desc,$category);
	  //mysql_query("update products set send_to_ebay = '0' where id = '{$product->id}'");
	}
	if (mysql_num_rows($res)) {
	  echo "Sending Email\n";
	  $item->sendEmail();
	}
//lib/ebay_end.php
  include "setincludepath.php";
if ($_SERVER['argv'][1]){
  $connection = mysql_connect( ... );
	mysql_select_db('yourapp_production');
	$res = mysql_query("select id,ebay_id,image_number from products where image_number = '{$_SERVER['argv'][1]}' and is_on_ebay = 1");
	$product = mysql_fetch_object($res);
  if ($product) {
    $ei = new EndItem();
    $ei->dispatchCall($product);
    $ei->sendEmail();
  }
}
//lib/set_ebay_notify.php
include "setincludepath.php";
class SetNotifications extends EbatNs_Environment {
  public function dispatchCall() {
    $req = new SetNotificationPreferencesRequestType();
    $req->ApplicationDeliveryPreferences = new ApplicationDeliveryPreferencesType();
    $req->ApplicationDeliveryPreferences->setApplicationEnable('Enable');
    $req->ApplicationDeliveryPreferences->setApplicationURL('http://yourdomain/ebay_notify.php');
    //$req->ApplicationDeliveryPreferences->setAlertEmail("youremail");
    $req->ApplicationDeliveryPreferences->setAlertEnable("Enable");
    $details = new DeliveryURLDetailType();
    $details->setDeliveryURLName('Philatelic');
    $details->setDeliveryURL('http://yourdomain/ebay_notify.php?details=true');
    $details->setStatus('Enable');
    $req->ApplicationDeliveryPreferences->setDeliveryURLDetails($details,null);
    $user = new NotificationEnableArrayType();
    $notifs = array();
     // put all of the notices you want here.
    foreach (array('BidReceived',
                   'EndOfAuction',
                   'FixedPriceEndOfTransaction',
                   'FixedPriceTransaction',
                   'ItemListed',
                   'ItemSold',
                   'FeedbackReceived') as $event) {
      $n = new NotificationEnableType();
      $n->setEventType($event);
      $n->setEventEnable('Enable');
      $notifs[] = $n;
    }
    $user->setNotificationEnable($notifs,null);
    $req->setUserDeliveryPreferenceArray($user);
    return $this->proxy->SetNotificationPreferences($req);
  }
}
echo "Setting notifications\n";
$s = new SetNotifications();
print_r($s->dispatchCall());
echo "\n----------\nGetting notifications\n";
$h = new EbayHelper();
print_r($h->get_notification_preferences('User'));

With the notification handler, for some reason, it simply refused to parse the SOAP request, I couldn't figure it out, and no xml parsing would work, so instead of bashing my head against the wall, I wrote a dumb file parser. This should work for most needs, or if you want to try and get the notification to parse, be my guest.

//public/ebay_notify.php
include "../lib/setincludepath.php";
$body = file_get_contents("php://input");
//$body = file_get_contents("note.xml");
$lines = explode("\n",$body);
$actions = array('BidReceived',
                 'FixedPriceEndOfTransaction',
                 'FixedPriceTransaction',
                 'ItemSold');
$tags = array(
    'RecipientUserID',
    'ItemID',
    'NotificationEventName',
    'SKU'
  );
$buyerTags = array(
  'Email',
  'Name',
  'Street1',
  'Street2',
  'StateOrProvince',
  'Country',
  'Phone',
  'PostalCode'
);

$shippingTags = array(
  'ShippingService'
  );
$inBuyer = false;
$inShipping = false;
$notification = array();
foreach ($lines as $line) {
  
  if (eregi('Buyer',$line)) {
      $inBuyer = true;
  }
  if (eregi('/Buyer',$line)) {
      $inBuyer = false;
  }
  if (eregi('ShippingServiceSelected',$line)) {
      $inShipping = true;
  }
  if (eregi('/ShippingServiceSelected',$line)) {
      $inShipping = false;
  }
  
  if (!$inBuyer and !$inShipping) {
    foreach ($tags as $t) {
      if (eregi("<" . $t . ">",$line)) {
        $notification[$t] = rtrim(trim(str_replace("<$t>",'',str_replace("</$t>",'',$line))));
      }
    }
  } else if ($inBuyer) {
    foreach ($buyerTags as $bt) {
      if (eregi("<" . $bt . ">",$line)) {
        $buyer[$bt] = rtrim(trim(str_replace("<$bt>",'',str_replace("</$bt>",'',$line))));
      }
    }
  } else if ($inShipping) {
    foreach ($shippingTags as $st) {
      if (eregi("<" . $st . ">",$line)) {
        $buyer['Shipping'][$st] = rtrim(trim(str_replace("<$st>",'',str_replace("</$st>",'',$line))));
      }
    }
  }
  
}
$connection = mysql_connect( ...);
mysql_select_db('yourapp_production');
$sql = array();
foreach ($actions as $a) {
  if ($notification['NotificationEventName'] == $a) {
    $query = "update products set send_to_ebay = 0, available_on = NULL where ebay_id = '{$notification['ItemID']}' OR image_number = '{$notification['SKU']}'";
    mysql_query($query);
    //echo $query;
    $sql[] = $query;
  }
}
	mail("youremail","Ebay Notification Received",join("\n",$sql) . "\n\n------\n\n" . $body);

Finally, how does it all work:

1. Create a cronjob with crontab -e and add this line:
*/5 * * * * root php /var/www/yourrailsroot/lib/ebay_send.php > /dev/null
This will cause a cronjob to run every 5 minutes, forever, to poll the database.

2. In your spree code, you will need to add something like this:

Order.class_eval do
  state_machine :initial => 'in_progress' do
    after_transition :to => 'paid', :do => lambda { |order|
      order.line_items.each do |li|
        p = li.variant.product
        if p.is_on_ebay and p.ebay_id then
          system("php /var/www/yourrailsroot/lib/ebay_end.php #{p.image_number}")
        end
      end
    }
  end
end

As for sending items to ebay, you just have to flag them in the database by setting send_to_ebay = 1, however you want to do it is fine.

Or however you are doing it in your latest spree, as I recall, right after I switched to spree, they did what all ROR developers do, and broke everything, restructuring the entire thing. Why? "Because fuck backwards compatibility" is the motto of Rails. (I am using the newest spree on another clients store, and actually it really was an improvement and is easier to use.)

In looking over the code to accomplish all of this, and getting it working smoothly, I feel a bit like an idiot that it took me so damn long to grok it, but there you go.

Naturally, the above is not complete, you'll need to follow the discussions and tutorials and docs on developer.ebay.com and get your app keys setup. If you need some help, or have a question, post a comment.

About Jason

I am a 26 year old programmer living in the south of France. I currently work actively in the fields of Ruby/PHP/Javascript and server/website administration.
This entry was posted in PHP Tutorials, PHP/MySQL, Tips and Tricks, Web Design and tagged , , , . Bookmark the permalink.