Categories
eBay Integration

T-10 Seconds and Counting …

The eBay Integration for Zen Cart Plug-in is ready for its initial release. I’ve cleaned up the code and updated the online documentation.

Categories
eBay Integration

eBay Integration – Batch Update

Note: The syntax of the Batch Command Language has changed since this post was created. See https://jonrocket.com/ebayintegration/documentation-usage-tabs-tools-tab-batch-update-tool.html

In the past, I’ve found that I often wrote custom PHP code to make changes to many or all of our eBay listings at once. For example, sometimes we wanted a little more time to process orders. In that case, I would run a short PHP script that updated the handling time for each eBay listing.

Now that I am making our eBay Integration for Zen Cart plug-in available to others, I wanted to include a way to make it possible for users to make the same kind of updates without having to write a lot of PHP code.

So, I am including a “Batch Update” tool as a part of the plug-in. It allows you to execute a series of simple, yet powerful, commands to update your eBay listings (and Zen Cart products).

As an example, suppose you added a number of listings to eBay then discovered that one of the Item Specifics you specified was incorrect. You could use the following set of commands to change the values of the “Brand” in the Item Specifics to “Company Name” in every listing where it is misspelled as “Copany Name”:

WHERE ebay.itemspecifics INCLUDES 'Brand|Copany Name'
REMOVE 'Brand|Copany Name' FROM ebay.itemspecifics
ADD 'Brand|Company Name' TO ebay.itemspecifics

The WHERE command defines the initial working set. The working set is the collection of products that the following commands will operate on. Only products matching the expression in the WHERE command will be included in the working set.

In the above example, only products associated with eBay listings that have an Item Specific defined as ‘Brand’ => ‘Copany Name’ will be included in the working set.

If you wanted all products to be included in the working set, you could enter:

WHERE true

To include only products with associated eBay Listings, you could enter:

WHERE ebay.itemid NOT IS ''

All expressions in the batch language consist of a single operand or two operands around an operator. So, if you want to create a working set based on more than one attribute, you need to use multiple commands. For example, to create a working set of products listed on eBay with the word ‘Toy’ in there title, you could use:

WHERE ebay.itemid NOT IS ''
AND ebay.title CONTAINS 'Toy'

The AND command modifies the working set by removing any products that do not match the expression.

To add products to the working set, use the OR command:

WHERE false
OR true

The above example is a rather odd way of adding all products to the working set. The OR command adds all products matching the expression to the working set.

Expressions are always evaluated in the context of a product – once for every product. For example, you can define a variable with the SET command:

SET $title = ebay.title

You can display the value of the variable using the EVAL command:

WHERE true
SET $title = ebay.title
EVAL $title

The EVAL command will list the value for each product.

You can also use the SET command to change the value of an eBay listing attribute (or store attribute). For example:

WHERE ebay.title ENDS WITH ' Toy'
SET $title = ebay.title
SET ebay.title = ebay.title TRIM END ' Toy'
SET ebay.title = ebay.title APPEND ' Chew Toy'
EVAL $title
EVAL ebay.title

The above will change every eBay title that ends with ‘ Toy’ to a title that ends with ‘ Chew Toy.’ The $title variable will contain the original title and won’t be changed when the title is updated.

Unless you check the “Commit?” box before submitting the batch script, the script will not actually change any listings at eBay. This allows you to test a script without changing any of your listings. Once you are satisfied that the script works as intended, you can check the “Commit?” box then submit the form and the changes will actually be made.

Categories
eBay Integration

eBay Integration – Installer

I just started working on the procedures and instructions for installing the eBay Integration for ZenCart plug-in. This should be fairly straight-forward since the plug-in does not overwrite any of the core ZenCart files and does not modify any of the existing database tables.

Categories
eBay Integration

eBay Integration – Polishing

The eBay Integration project for Zen Cart is working well on our web site. But, before I make it available to others, I decided to make it work a little better.

The process for adding Zen Cart products to the eBay store was a little too cumbersome. I had a tab where you could list and search for products then select a product to edit. The editor allowed you to manage additional product information needed to list the items on eBay. Then, to actually add the products to eBay, you would select a menu option on the Tools tab. If any products failed to add, you would have to go back to the Products tab to edit the product before trying again.

I took some time to rewrite the code so that listing, searching, browsing, and editing products is much easier (and the code will be much easier to maintain). To add a product to eBay, you just click a button when the product is displayed. Any errors are reported on the same page so you can easily edit the product to fix them and try again.

For now, at least, once the product is listed on eBay the eBay Integration system keeps the inventory in sync between the Zen Cart store and eBay. But, if you modify a product attribute – such as the description – in Zen Cart, the corresponding eBay listing will not automatically be updated. I think that will work okay and be less confusing. Currently, I review each eBay listing after it is completed and tweak it as necessary. Even with this extra step, listing products using this plug in for Zen Cart is much easier and quicker than entering then through eBay’s web site.

Categories
eBay Integration

eBay Integration – Two Steps Forward, One Step Back

Recently I had to take a step back to address some issues with the development of the eBay Integration plug-in for Zen Cart.

The main reason to reconsider how some things have been implemented so far was due to eBay requiring the use of the oAuth protocol to call some API methods. A secondary reason was to restructure the code to be more maintainable and extensible.

Implementing the oAuth protocol was challenging because the documentation is confusing and there is a lack of PHP examples to follow. But, I finally got it working.

Here is the class I developed that the code uses to get an access token and refresh it when necessary.

<?php
// -----
// eBay Integration plugin for Zen Cart
//
// Copyright (c) 2021, Roger Smith (sales@jonrocket.com)
//

// -----
// oAuth Methods
//

require_once DIR_FS_CATALOG . 'cron/ebayincludes/utilities/ebayutils.php';

function ebayGetAccessToken()
{
  $token = ebayGetConfiguration('ACCESS_TOKEN');
  if ($token != '')
  {
    $expires = DateTime::createFromFormat('Y-m-d H:i:s', ebayGetConfiguration('ACCESS_TOKEN_EXPIRES'));
    $now     = new DateTime();
    $diff    = ebayDateIntervalToSeconds(date_diff($expires, $now));
    if ($diff < 120 || ($now > $expires))
    {
      ebayRefreshAccessToken();
      $token = ebayGetConfiguration('ACCESS_TOKEN');
    }
  }
  else
  {
    ebayLog(EBAY_LOG_ERROR_NO_ACCESS_TOKEN, true);
  }
  return $token;
}

function ebayRefreshAccessToken()
{
  $refreshToken = ebayGetConfiguration('REFRESH_TOKEN');
  if ($refreshToken != '')
  {
    $expires = DateTime::createFromFormat('Y-m-d H:i:s', ebayGetConfiguration('REFRESH_TOKEN_EXPIRES'));
    $now     = new DateTime();
    $diff    = ebayDateIntervalToSeconds(date_diff($expires, $now));
    if ($diff < 60)
    {
      ebayLog(EBAY_LOG_ERROR_REFRESH_TOKEN_EXPIRED, true);
      return;
    }
    else if ($diff < (30 * 24 * 60 * 60))
    {
      $lastWarning = ebayGetConfiguration('LAST_REFRESH_EXPIRES_WARNING');
      if ($lastWarning == '' || (time() - intval($lastWarning)) > (24 * 60 * 60))
      {
        ebayLog(EBAY_LOG_ERROR_REFRESH_TOKEN_EXPIRING, false);
        ebaySetConfiguration('LAST_REFRESH_EXPIRES_WARNING', strval(time()));
      }
    }
  }
  else
  {
    ebayLog(EBAY_LOG_ERROR_NO_REFRESH_TOKEN, true);
  }
  $now           = new DateTimeImmutable();
  $scope         = implode(' ', array(
      'https://api.ebay.com/oauth/api_scope/sell.inventory',
      'https://api.ebay.com/oauth/api_scope/sell.account',
      'https://api.ebay.com/oauth/api_scope/sell.account.readonly',
      'https://api.ebay.com/oauth/api_scope/sell.fulfillment'
  ));
  $appID         = ebayGetConfiguration('PROD_APPID');
  $certID        = ebayGetConfiguration('PROD_CERTID');
  $ruName        = ebayGetConfiguration('PROD_RU_NAME');
  $url           = "https://api.ebay.com/identity/v1/oauth2/token";
  $authorization = base64_encode($appID . ':' . $certID);
  $curlHandle    = curl_init($url);
  curl_setopt($curlHandle, CURLOPT_HTTPHEADER, array(
      'Content-Type: application/x-www-form-urlencoded',
      'Authorization: Basic ' . $authorization
  ));
  $postFields = http_build_query(array(
      'grant_type' => 'refresh_token',
      'refresh_token' => $refreshToken,
      'scope' => $scope
  ));
  curl_setopt($curlHandle, CURLHEADER_SEPARATE, true);
  curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, false);
  curl_setopt($curlHandle, CURLOPT_POST, 1);
  curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $postFields);
  $response = curl_exec($curlHandle);
  curl_close($curlHandle);
  $json = json_decode($response, true);
  if ($json != null)
  {
    @$error = $json['error'];
    if ($error != '')
    {
      ebayLog(EBAY_LOG_ERROR_REFRESH_TOKEN . ' (' . $error . ' - ' . $json['error_description'] . ')', true);
      return;
    }
    $accessToken = $json['access_token'];
    ebaySetConfiguration('ACCESS_TOKEN', $accessToken);
    $accessTokenExpiresIn = $json['expires_in'];
    $expires              = new DateInterval('PT' . $accessTokenExpiresIn . 'S');
    $accessTokenExpires   = $now->add($expires);
    ebaySetConfiguration('ACCESS_TOKEN_EXPIRES', $accessTokenExpires->format('Y-m-d H:i:s'));
  }
}

function ebaySetAccessToken($code)
{
  $now           = new DateTimeImmutable();
  $appID         = ebayGetConfiguration('PROD_APPID');
  $certID        = ebayGetConfiguration('PROD_CERTID');
  $ruName        = ebayGetConfiguration('PROD_RU_NAME');
  $url           = "https://api.ebay.com/identity/v1/oauth2/token";
  $authorization = base64_encode($appID . ':' . $certID);
  $curlHandle    = curl_init($url);
  curl_setopt($curlHandle, CURLOPT_HTTPHEADER, array(
      'Content-Type: application/x-www-form-urlencoded',
      'Authorization: Basic ' . $authorization
  ));
  $postFields = http_build_query(array(
      'grant_type' => 'authorization_code',
      'code' => urldecode($code),
      'redirect_uri' => $ruName
  ));
  
  curl_setopt($curlHandle, CURLHEADER_SEPARATE, true);
  curl_setopt($curlHandle, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($curlHandle, CURLOPT_SSL_VERIFYPEER, false);
  curl_setopt($curlHandle, CURLOPT_POST, 1);
  curl_setopt($curlHandle, CURLOPT_POSTFIELDS, $postFields);
  $response = curl_exec($curlHandle);
  curl_close($curlHandle);
  $json = json_decode($response, true);
  if ($json != null)
  {
    @$error = $json['error'];
    if ($error != '')
    {
      echo '<p><b>Error:</b> ' . $error . ' - ' . $json['error_description'] . '</p>';
      return;
    }
    $accessToken = $json['access_token'];
    ebaySetConfiguration('ACCESS_TOKEN', $accessToken);
    $accessTokenExpiresIn = $json['expires_in'];
    $expires              = new DateInterval('PT' . $accessTokenExpiresIn . 'S');
    $accessTokenExpires   = $now->add($expires);
    ebaySetConfiguration('ACCESS_TOKEN_EXPIRES', $accessTokenExpires->format('Y-m-d H:i:s'));
    $refreshToken = $json['refresh_token'];
    ebaySetConfiguration('REFRESH_TOKEN', $refreshToken);
    $refreshTokenExpiresIn = $json['refresh_token_expires_in'];
    $expires               = new DateInterval('PT' . $refreshTokenExpiresIn . 'S');
    $refreshTokenExpires   = $now->add($expires);
    ebaySetConfiguration('REFRESH_TOKEN_EXPIRES', $refreshTokenExpires->format('Y-m-d H:i:s'));
  }
}
Code language: HTML, XML (xml)

The ebayGetAccessToken method returns the current access token unless it has expired or is about to expire. If it has expired or is about to expire, the refresh token is used to obtain a new access token.

The initial access token and refresh token are retrieved by the ebaySetAccessToken method. A special code returned by eBay when the user logs into eBay and authorizes the plug-in to access the user’s eBay info is passed to the method. The code is used to call an eBay method to retrieve the tokens.

I had been putting most of the code for the admin interface in a single source file. This was quickly becoming unwieldly. So, I recently took some time to clean it up by moving some methods to a utility file and encapsulating things the GUI tabs into individual classes in separate files.

Categories
Zen Cart Upgrades

Improving Zen Cart Search Results

You Can’t Always Find What You Want ….

Zen Cart’s search function doesn’t work very well. Search results are not ordered by relevance and many search terms produce no results even though there are products that should be listed.

In this article I describe how I made the search function work better for our model rocket store web site.

Do What I Say, Not What I Do ….

The method I describe here involves directly modifying Zen Cart core files. Generally, it isn’t a good idea to modify the core files of an application since it makes upgrading more difficult and error-prone.  But the upgrade process for Zen Cart is already error-prone since many themes directly modify core files.  Just be aware that by making the changes described here, you may be making things more difficult in the future.

The modifications described here are based on version 1.5.7 of Zen Cart.  If you are using a later version, you may need to do things a little differently than described here. 

Here is a list of the files that will be updated (replace [admin] with the name of your admin folder):

  • includes/modules/pages/advanced_search_result/header_php.php
  • [admin]/includes/functions/general.php
  • [admin]/includes/modules/document_product/preview_info.php

Fixing the Order of the Results ….

The first problem is that the order of the results isn’t based on how well the product matches the search term. For example, a product that has a title matching the search term should be listed before one that uses the search term in its description.  But, the order of the results seems random instead.

“Rob” of www.funkyraw.com offers an elegant solution in a post in the Zen Cart forum based on MySQL’s FULLTEXT search feature. 

I expanded on his technique a bit by adding a text field where you can enter additional text to be searched that isn’t displayed in the store and by slightly changing the order of the results.

Database Updates

We are going to add a column to the table that holds product descriptions and add a Full Text index to the table.

Using phpMyAdmin, go to the Structure tab of your products_description table. Add a products_search column with a type of “text.”

On the row for products_name click the Fulltext (“T”) icon (it may be under the “More” menu).  Next, select the check boxes next to the products_description and products_search field.  Then click the Fulltext link under the table and create a Fulltext index on the two columns .

File Update: includes/modules/pages/advanced_search_result/header_php.php

Edit the includes/modules/pages/advanced_search_result/header_php.php file.  Around line 219, find the following code:

// Notifier Point
$zco_notifier->notify('NOTIFY_SEARCH_SELECT_STRING');

//  $from_str = "from " . TABLE_PRODUCTS . " p left join " . TABLE_MANUFACTURERS . " m using(manufacturers_id), " . TABLE_PRODUCTS_DESCRIPTION . " pd left join " . TABLE_SPECIALS . " s on p.products_id = s.products_id, " . TABLE_CATEGORIES . " c, " . TABLE_PRODUCTS_TO_CATEGORIES . " p2c";

$from_str = "FROM (" . TABLE_PRODUCTS . " pCode language: PHP (php)

Replace the above code with:

// Notifier Point
$zco_notifier->notify('NOTIFY_SEARCH_SELECT_STRING');

//  $from_str = "from " . TABLE_PRODUCTS . " p left join " . TABLE_MANUFACTURERS . " m using(manufacturers_id), " . TABLE_PRODUCTS_DESCRIPTION . " pd left join " . TABLE_SPECIALS . " s on p.products_id = s.products_id, " . TABLE_CATEGORIES . " c, " . TABLE_PRODUCTS_TO_CATEGORIES . " p2c";

// FullText Ranking code by Rob - www.funkyraw.com
$from_str = ",  MATCH(pd.products_name) AGAINST(:keywords) AS rank1, MATCH(pd.products_description, pd.products_search) AGAINST(:keywords) AS rank2 ";
$from_str = $db->bindVars($from_str, ':keywords', stripslashes($_GET['keyword']), 'string');
//end FullText ranking code

$from_str .= "FROM (" . TABLE_PRODUCTS . " pCode language: PHP (php)

The first few lines are existing code that is not changed just to show you where to add the new code. The new code is added where you find the following in the existing file:

// Notifier Point
$zco_notifier->notify('NOTIFY_SEARCH_SELECT_STRING');

//  $from_str = "from " . TABLE_PRODUCTS . " p left join " . TABLE_MANUFACTURERS . " m using(manufacturers_id), " . TABLE_PRODUCTS_DESCRIPTION . " pd left join " . TABLE_SPECIALS . " s on p.products_id = s.products_id, " . TABLE_CATEGORIES . " c, " . TABLE_PRODUCTS_TO_CATEGORIES . " p2c";
Code language: PHP (php)

The “// $from_str …” comment is dead code that is in the original file for version 1.57 (it might not be in a later version). Note that a “.” is added before the “=” in the last line of the new code.

Look farther down in the file for the following code:

if (isset($_GET['search_in_description']) && ($_GET['search_in_description'] == '1')) {
 $where_str .= " OR pd.products_description 
                 LIKE '%:keywords%'";
Code language: PHP (php)

Replace that code with:

// Full Text Search
// if (isset($_GET['search_in_description']) && ($_GET['search_in_description'] == '1')) {
//  $where_str .= " OR pd.products_description
//                  LIKE '%:keywords%'";

if (isset($_GET['search_in_description']) && ($_GET['search_in_description'] == '1')) {
  $where_str .= " OR pd.products_description
          LIKE '%:keywords%' OR pd.products_search
          LIKE '%:keywords%'";
// End ROGERCode language: PHP (php)

Now, closer to the end of the file, somewhere near line 420, and replace:

$order_str .= " order by p.products_sort_order, pd.products_name";Code language: PHP (php)

With:

// Full text search
// $order_str .= " order by p.products_sort_order, pd.products_name";
$order_str .= " order by rank1 DESC, rank2 DESC, p.products_sort_order, pd.products_name";Code language: PHP (php)

Okay, with these changes, the order of products returned for searches will be nicer.

Now, to make it easier to edit the extra search text, we need to update the Edit Product admin page to allow you to enter text in the new products_search column. Basically, everywhere the code deals with the products_description column, we duplicate it for the new products_search column.

File Update: [admin]/includes/functions/general.php

In the [admin]/includes/functions/general.php file add the following new method:

// Fulltext Search
  function zen_get_products_search($product_id, $language_id) {
    global $db;
    $product = $db->Execute("SELECT products_search
                             FROM " . TABLE_PRODUCTS_DESCRIPTION . "
                             WHERE products_id = " . (int)$product_id . "
                             AND language_id = " . (int)$language_id);
    if ($prodif (empty($products_description)) $products_description = [];
if (empty($products_search)) $products_search = [];ct->EOF) return '';
    return $product->fields['products_search'];
  }Code language: PHP (php)

File Update: [admin]/includes/modules/document_product/preview_info.php

Now, edit the [admin]/includes/modules/document_product/preview_info.php file. Near the top, find the code:

if (empty($products_description)) $products_description = [];
Code language: PHP (php)

Replace it with:

if (empty($products_description)) $products_description = [];
if (empty($products_search)) $products_search = [];Code language: PHP (php)

Around line 20, replace this code:

  $products_description = $_POST['products_description'];
Code language: PHP (php)

With:

  $products_description = $_POST['products_description'];
  $products_search = $_POST['products_search'];Code language: PHP (php)

Near line 28, replace:

  $product = $db->Execute("SELECT p.*,
                                  pd.language_id, pd.products_name, pd.products_description, pd.products_url
                           FROM " . TABLE_PRODUCTS . " p,
                                " . TABLE_PRODUCTS_DESCRIPTION . " pd
                           WHERE p.products_id = pd.products_id
                           AND p.products_id = " . (int)$_GET['pID']);
Code language: PHP (php)

With:

  $product = $db->Execute("SELECT p.*,
                                  pd.language_id, pd.products_name, pd.products_description, pd.products_search, pd.products_url
                           FROM " . TABLE_PRODUCTS . " p,
                                " . TABLE_PRODUCTS_DESCRIPTION . " pd
                           WHERE p.products_id = pd.products_id
                           AND p.products_id = " . (int)$_GET['pID']);
Code language: PHP (php)

Around line 41, replace:

    $products_description[$prod['language_id']] = $prod['products_description'];
Code language: PHP (php)

With:

    $products_description[$prod['language_id']] = $prod['products_description'];
    $products_search[$prod['language_id']] = $prod['products_search'];
Code language: PHP (php)

Near line 54, replace:

    for ($i = 0, $n = count($languages); $i < $n; $i++) {
      if (isset($_GET['read']) && ($_GET['read'] == 'only')) {
        $pInfo->products_name = zen_get_products_name($pInfo->products_id, $languages[$i]['id']);
        $pInfo->products_description = zen_get_products_description($pInfo->products_id, $languages[$i]['id']);
        $pInfo->products_url = zen_get_products_url($pInfo->products_id, $languages[$i]['id']);
      } else {
        $pInfo->products_name = zen_db_prepare_input($products_name[$languages[$i]['id']]);
        $pInfo->products_description = zen_db_prepare_input($products_description[$languages[$i]['id']]);
        $pInfo->products_url = zen_db_prepare_input($products_url[$languages[$i]['id']]);
      }
Code language: PHP (php)

With:

    for ($i = 0, $n = count($languages); $i < $n; $i++) {
      if (isset($_GET['read']) && ($_GET['read'] == 'only')) {
        $pInfo->products_name = zen_get_products_name($pInfo->products_id, $languages[$i]['id']);
        $pInfo->products_description = zen_get_products_description($pInfo->products_id, $languages[$i]['id']);
        $pInfo->products_search = zen_get_products_search($pInfo->products_id, $languages[$i]['id']);
        $pInfo->products_url = zen_get_products_url($pInfo->products_id, $languages[$i]['id']);
      } else {
        $pInfo->products_name = zen_db_prepare_input($products_name[$languages[$i]['id']]);
        $pInfo->products_description = zen_db_prepare_input($products_description[$languages[$i]['id']]);
        $pInfo->products_search = zen_db_prepare_input($products_search[$languages[$i]['id']]);
        $pInfo->products_url = zen_db_prepare_input($products_url[$languages[$i]['id']]);
      }
Code language: PHP (php)

Around line 160, replace:

        for ($i = 0, $n = count($languages); $i < $n; $i++) {
          echo zen_draw_hidden_field('products_name[' . $languages[$i]['id'] . ']', htmlspecialchars(stripslashes($products_name[$languages[$i]['id']]), ENT_COMPAT, CHARSET, TRUE));
          echo zen_draw_hidden_field('products_description[' . $languages[$i]['id'] . ']', htmlspecialchars(stripslashes($products_description[$languages[$i]['id']]), ENT_COMPAT, CHARSET, TRUE));
          echo zen_draw_hidden_field('products_url[' . $languages[$i]['id'] . ']', htmlspecialchars(stripslashes($products_url[$languages[$i]['id']]), ENT_COMPAT, CHARSET, TRUE));
        }
Code language: PHP (php)

With:

        for ($i = 0, $n = count($languages); $i < $n; $i++) {
          echo zen_draw_hidden_field('products_name[' . $languages[$i]['id'] . ']', htmlspecialchars(stripslashes($products_name[$languages[$i]['id']]), ENT_COMPAT, CHARSET, TRUE));
          echo zen_draw_hidden_field('products_description[' . $languages[$i]['id'] . ']', htmlspecialchars(stripslashes($products_description[$languages[$i]['id']]), ENT_COMPAT, CHARSET, TRUE));
          echo zen_draw_hidden_field('products_search[' . $languages[$i]['id'] . ']', htmlspecialchars(stripslashes($products_search[$languages[$i]['id']]), ENT_COMPAT, CHARSET, TRUE));
          echo zen_draw_hidden_field('products_url[' . $languages[$i]['id'] . ']', htmlspecialchars(stripslashes($products_url[$languages[$i]['id']]), ENT_COMPAT, CHARSET, TRUE));
        }
Code language: PHP (php)

File Update: [admin]/includes/modules/product/collect_info.php

In the [admin]/includes/modules/product/collect_info.php file, near line 11, change:

$parameters = [
  'products_name' => '',
  'products_description' => '',
  'products_url' => '',
  'products_id' => '',
  'products_quantity' => '0',
  'products_model' => '',
  'products_image' => '',
  'products_price' => '0.0000',
  'products_virtual' => DEFAULT_PRODUCT_PRODUCTS_VIRTUAL,
  'products_weight' => '0',
  'products_date_added' => '',
  'products_last_modified' => '',
  'products_date_available' => '',
  'products_status' => '1',
  'products_tax_class_id' => DEFAULT_PRODUCT_TAX_CLASS_ID,
  'manufacturers_id' => '',
  'products_quantity_order_min' => '1',
  'products_quantity_order_units' => '1',
  'products_priced_by_attribute' => '0',
  'product_is_free' => '0',
  'product_is_call' => '0',
  'products_quantity_mixed' => '1',
  'product_is_always_free_shipping' => DEFAULT_PRODUCT_PRODUCTS_IS_ALWAYS_FREE_SHIPPING,
  'products_qty_box_status' => PRODUCTS_QTY_BOX_STATUS,
  'products_quantity_order_max' => '0',
  'products_sort_order' => '0',
  'products_discount_type' => '0',
  'products_discount_type_from' => '0',
  'products_price_sorter' => '0',
  'master_categories_id' => '',
];
Code language: PHP (php)

To:

$parameters = [
  'products_name' => '',
  'products_description' => '',
  'products_search' => '',
  'products_url' => '',
  'products_id' => '',
  'products_quantity' => '0',
  'products_model' => '',
  'products_image' => '',
  'products_price' => '0.0000',
  'products_virtual' => DEFAULT_PRODUCT_PRODUCTS_VIRTUAL,
  'products_weight' => '0',
  'products_date_added' => '',
  'products_last_modified' => '',
  'products_date_available' => '',
  'products_status' => '1',
  'products_tax_class_id' => DEFAULT_PRODUCT_TAX_CLASS_ID,
  'manufacturers_id' => '',
  'products_quantity_order_min' => '1',
  'products_quantity_order_units' => '1',
  'products_priced_by_attribute' => '0',
  'product_is_free' => '0',
  'product_is_call' => '0',
  'products_quantity_mixed' => '1',
  'product_is_always_free_shipping' => DEFAULT_PRODUCT_PRODUCTS_IS_ALWAYS_FREE_SHIPPING,
  'products_qty_box_status' => PRODUCTS_QTY_BOX_STATUS,
  'products_quantity_order_max' => '0',
  'products_sort_order' => '0',
  'products_discount_type' => '0',
  'products_discount_type_from' => '0',
  'products_price_sorter' => '0',
  'master_categories_id' => '',
];
Code language: PHP (php)

Near line 48, change:

  $product = $db->Execute("SELECT pd.products_name, pd.products_description, pd.products_url,
                                  p.*, 
                                  date_format(p.products_date_available, '" .  zen_datepicker_format_forsql() . "') as products_date_available
                           FROM " . TABLE_PRODUCTS . " p,
                                " . TABLE_PRODUCTS_DESCRIPTION . " pd
                           WHERE p.products_id = " . (int)$_GET['pID'] . "
                           AND p.products_id = pd.products_id
                           AND pd.language_id = " . (int)$_SESSION['languages_id']);
Code language: PHP (php)

To:

  $product = $db->Execute("SELECT pd.products_name, pd.products_description, pd.products_search, pd.products_url,
                                  p.*, 
                                  date_format(p.products_date_available, '" .  zen_datepicker_format_forsql() . "') as products_date_available
                           FROM " . TABLE_PRODUCTS . " p,
                                " . TABLE_PRODUCTS_DESCRIPTION . " pd
                           WHERE p.products_id = " . (int)$_GET['pID'] . "
                           AND p.products_id = pd.products_id
                           AND pd.language_id = " . (int)$_SESSION['languages_id']);
Code language: PHP (php)

Around line 61, replace:

  $products_description = isset($_POST['products_description']) ? $_POST['products_description'] : '';
Code language: PHP (php)

With:

  $products_description = isset($_POST['products_description']) ? $_POST['products_description'] : '';
  $products_search = isset($_POST['products_search']) ? $_POST['products_search'] : '';
Code language: PHP (php)

Around line 323, replace:

  <div class="form-group">
      <p class="col-sm-3 control-label"><?php echo TEXT_PRODUCTS_DESCRIPTION; ?></p>
    <div class="col-sm-9 col-md-6">
        <?php
        for ($i = 0, $n = count($languages); $i < $n; $i++) {
          ?>
        <div class="input-group">
          <span class="input-group-addon">
              <?php echo zen_image(DIR_WS_CATALOG_LANGUAGES . $languages[$i]['directory'] . '/images/' . $languages[$i]['image'], $languages[$i]['name']); ?>
          </span>
          <?php echo zen_draw_textarea_field('products_description[' . $languages[$i]['id'] . ']', 'soft', '100', '30', htmlspecialchars((isset($products_description[$languages[$i]['id']])) ? stripslashes($products_description[$languages[$i]['id']]) : zen_get_products_description($pInfo->products_id, $languages[$i]['id']), ENT_COMPAT, CHARSET, TRUE), 'class="editorHook form-control"'); ?>
        </div>
        <br>
        <?php
      }
      ?>
    </div>
  </div>
Code language: JavaScript (javascript)

With:

  <div class="form-group">
      <p class="col-sm-3 control-label"><?php echo TEXT_PRODUCTS_DESCRIPTION; ?></p>
    <div class="col-sm-9 col-md-6">
        <?php
        for ($i = 0, $n = count($languages); $i < $n; $i++) {
          ?>
        <div class="input-group">
          <span class="input-group-addon">
              <?php echo zen_image(DIR_WS_CATALOG_LANGUAGES . $languages[$i]['directory'] . '/images/' . $languages[$i]['image'], $languages[$i]['name']); ?>
          </span>
          <?php echo zen_draw_textarea_field('products_description[' . $languages[$i]['id'] . ']', 'soft', '100', '30', htmlspecialchars((isset($products_description[$languages[$i]['id']])) ? stripslashes($products_description[$languages[$i]['id']]) : zen_get_products_description($pInfo->products_id, $languages[$i]['id']), ENT_COMPAT, CHARSET, TRUE), 'class="editorHook form-control"'); ?>
        </div>
        <br>
        <?php
      }
      ?>
    </div>
  </div>
  <div class="form-group">
      <p class="col-sm-3 control-label">Extra Search Text</p>
    <div class="col-sm-9 col-md-6">
        <?php
        for ($i = 0, $n = count($languages); $i < $n; $i++) {
          ?>
        <div class="input-group">
          <span class="input-group-addon">
              <?php echo zen_image(DIR_WS_CATALOG_LANGUAGES . $languages[$i]['directory'] . '/images/' . $languages[$i]['image'], $languages[$i]['name']); ?>
          </span>
          <?php echo zen_draw_textarea_field('products_search[' . $languages[$i]['id'] . ']', 'soft', '100', '30', htmlspecialchars((isset($products_search[$languages[$i]['id']])) ? stripslashes($products_search[$languages[$i]['id']]) : zen_get_products_search($pInfo->products_id, $languages[$i]['id']), ENT_COMPAT, CHARSET, TRUE), 'class="editorHook form-control"'); ?>
        </div>
        <br>
        <?php
      }
      ?>
    </div>
  </div>
Code language: JavaScript (javascript)

File Update: [admin]/includes/modules/product/collect_info.php

In the [admin]/includes/modules/product/collect_info.php file, near line 11, change:

$parameters = [
  'products_name' => '',
  'products_description' => '',
  'products_url' => '',
  'products_id' => '',
  'products_quantity' => '0',
  'products_model' => '',
  'products_image' => '',
  'products_price' => '0.0000',
  'products_virtual' => DEFAULT_PRODUCT_PRODUCTS_VIRTUAL,
  'products_weight' => '0',
  'products_date_added' => '',
  'products_last_modified' => '',
  'products_date_available' => '',
  'products_status' => '1',
  'products_tax_class_id' => DEFAULT_PRODUCT_TAX_CLASS_ID,
  'manufacturers_id' => '',
  'products_quantity_order_min' => '1',
  'products_quantity_order_units' => '1',
  'products_priced_by_attribute' => '0',
  'product_is_free' => '0',
  'product_is_call' => '0',
  'products_quantity_mixed' => '1',
  'product_is_always_free_shipping' => DEFAULT_PRODUCT_PRODUCTS_IS_ALWAYS_FREE_SHIPPING,
  'products_qty_box_status' => PRODUCTS_QTY_BOX_STATUS,
  'products_quantity_order_max' => '0',
  'products_sort_order' => '0',
  'products_discount_type' => '0',
  'products_discount_type_from' => '0',
  'products_price_sorter' => '0',
  'master_categories_id' => '',
];
Code language: PHP (php)

To:

$parameters = [
  'products_name' => '',
  'products_description' => '',
  'products_search' => '',
  'products_url' => '',
  'products_id' => '',
  'products_quantity' => '0',
  'products_model' => '',
  'products_image' => '',
  'products_price' => '0.0000',
  'products_virtual' => DEFAULT_PRODUCT_PRODUCTS_VIRTUAL,
  'products_weight' => '0',
  'products_date_added' => '',
  'products_last_modified' => '',
  'products_date_available' => '',
  'products_status' => '1',
  'products_tax_class_id' => DEFAULT_PRODUCT_TAX_CLASS_ID,
  'manufacturers_id' => '',
  'products_quantity_order_min' => '1',
  'products_quantity_order_units' => '1',
  'products_priced_by_attribute' => '0',
  'product_is_free' => '0',
  'product_is_call' => '0',
  'products_quantity_mixed' => '1',
  'product_is_always_free_shipping' => DEFAULT_PRODUCT_PRODUCTS_IS_ALWAYS_FREE_SHIPPING,
  'products_qty_box_status' => PRODUCTS_QTY_BOX_STATUS,
  'products_quantity_order_max' => '0',
  'products_sort_order' => '0',
  'products_discount_type' => '0',
  'products_discount_type_from' => '0',
  'products_price_sorter' => '0',
  'master_categories_id' => '',
];
Code language: PHP (php)

Near line 47, replace:

if (isset($_GET['pID']) && empty($_POST)) {
  $product = $db->Execute("SELECT pd.products_name, pd.products_description, pd.products_url,
                                  p.*, 
                                  date_format(p.products_date_available, '" .  zen_datepicker_format_forsql() . "') as products_date_available
                           FROM " . TABLE_PRODUCTS . " p,
                                " . TABLE_PRODUCTS_DESCRIPTION . " pd
                           WHERE p.products_id = " . (int)$_GET['pID'] . "
                           AND p.products_id = pd.products_id
                           AND pd.language_id = " . (int)$_SESSION['languages_id']);

  $pInfo->updateObjectInfo($product->fields);
} elseif (zen_not_null($_POST)) {
  $pInfo->updateObjectInfo($_POST);
  $products_name = isset($_POST['products_name']) ? $_POST['products_name'] : '';
  $products_description = isset($_POST['products_description']) ? $_POST['products_description'] : '';
  $products_url = isset($_POST['products_url']) ? $_POST['products_url'] : '';
}
Code language: PHP (php)

With:

if (isset($_GET['pID']) && empty($_POST)) {
  $product = $db->Execute("SELECT pd.products_name, pd.products_description, pd.products_search, pd.products_url,
                                  p.*, 
                                  date_format(p.products_date_available, '" .  zen_datepicker_format_forsql() . "') as products_date_available
                           FROM " . TABLE_PRODUCTS . " p,
                                " . TABLE_PRODUCTS_DESCRIPTION . " pd
                           WHERE p.products_id = " . (int)$_GET['pID'] . "
                           AND p.products_id = pd.products_id
                           AND pd.language_id = " . (int)$_SESSION['languages_id']);

  $pInfo->updateObjectInfo($product->fields);
} elseif (zen_not_null($_POST)) {
  $pInfo->updateObjectInfo($_POST);
  $products_name = isset($_POST['products_name']) ? $_POST['products_name'] : '';
  $products_description = isset($_POST['products_description']) ? $_POST['products_description'] : '';
  $products_search = isset($_POST['products_search']) ? $_POST['products_search'] : '';
  $products_url = isset($_POST['products_url']) ? $_POST['products_url'] : '';
}
Code language: PHP (php)

Near line 323, change:

  <div class="form-group">
      <p class="col-sm-3 control-label"><?php echo TEXT_PRODUCTS_DESCRIPTION; ?></p>
    <div class="col-sm-9 col-md-6">
        <?php
        for ($i = 0, $n = count($languages); $i < $n; $i++) {
          ?>
        <div class="input-group">
          <span class="input-group-addon">
              <?php echo zen_image(DIR_WS_CATALOG_LANGUAGES . $languages[$i]['directory'] . '/images/' . $languages[$i]['image'], $languages[$i]['name']); ?>
          </span>
          <?php echo zen_draw_textarea_field('products_description[' . $languages[$i]['id'] . ']', 'soft', '100', '30', htmlspecialchars((isset($products_description[$languages[$i]['id']])) ? stripslashes($products_description[$languages[$i]['id']]) : zen_get_products_description($pInfo->products_id, $languages[$i]['id']), ENT_COMPAT, CHARSET, TRUE), 'class="editorHook form-control"'); ?>
        </div>
        <br>
        <?php
      }
      ?>
    </div>
  </div>
Code language: JavaScript (javascript)

To:

  <div class="form-group">
      <p class="col-sm-3 control-label"><?php echo TEXT_PRODUCTS_DESCRIPTION; ?></p>
    <div class="col-sm-9 col-md-6">
        <?php
        for ($i = 0, $n = count($languages); $i < $n; $i++) {
          ?>
        <div class="input-group">
          <span class="input-group-addon">
              <?php echo zen_image(DIR_WS_CATALOG_LANGUAGES . $languages[$i]['directory'] . '/images/' . $languages[$i]['image'], $languages[$i]['name']); ?>
          </span>
          <?php echo zen_draw_textarea_field('products_description[' . $languages[$i]['id'] . ']', 'soft', '100', '30', htmlspecialchars((isset($products_description[$languages[$i]['id']])) ? stripslashes($products_description[$languages[$i]['id']]) : zen_get_products_description($pInfo->products_id, $languages[$i]['id']), ENT_COMPAT, CHARSET, TRUE), 'class="editorHook form-control"'); ?>
        </div>
        <br>
        <?php
      }
      ?>
    </div>
  </div>
  <div class="form-group">
      <p class="col-sm-3 control-label">Extra Search Text</p>
    <div class="col-sm-9 col-md-6">
        <?php
        for ($i = 0, $n = count($languages); $i < $n; $i++) {
          ?>
        <div class="input-group">
          <span class="input-group-addon">
              <?php echo zen_image(DIR_WS_CATALOG_LANGUAGES . $languages[$i]['directory'] . '/images/' . $languages[$i]['image'], $languages[$i]['name']); ?>
          </span>
          <?php echo zen_draw_textarea_field('products_search[' . $languages[$i]['id'] . ']', 'soft', '100', '30', htmlspecialchars((isset($products_search[$languages[$i]['id']])) ? stripslashes($products_search[$languages[$i]['id']]) : zen_get_products_search($pInfo->products_id, $languages[$i]['id']), ENT_COMPAT, CHARSET, TRUE), 'class="editorHook form-control"'); ?>
        </div>
        <br>
        <?php
      }
      ?>
    </div>
  </div>
Code language: JavaScript (javascript)

File Update: [admin]/includes/modules/product/preview_info.php

In the [admin]/includes/modules/product/preview_info.php file, near line 12, change:

if (empty($products_description)) $products_description = [];
Code language: PHP (php)

To:

if (empty($products_description)) $products_description = [];
if (empty($products_search)) $products_search = [];
Code language: PHP (php)

Around line 20, change:

  $products_description = $_POST['products_description'];
Code language: PHP (php)

To:

  $products_description = $_POST['products_description'];
  $products_search = $_POST['products_search'];
Code language: PHP (php)

Conclusion

Now, when a visitor searches for a product on your Zen Cart store web site the search results will be more relevant and ordered by relevancy.

Categories
eBay Integration

eBay Integration – Creating the Plug-In

The latest version of Zen Cart includes a new method of defining plug-ins. Even though it is still being developed, subject to change, and not well documented, I’m using it for my eBay Integration plug-in because it has a big advantage over the older method of defining a plug-in.

In the past, a plug-in’s files were stored in multiple places in the Zen Cart directory structure and some plug-in files actually overwrote existing “core” files. This made updating and maintaining both the plug-in and Zen Cart difficult. The new method puts the plug-in files in one area and offers a cleaner way to upgrade.

Unfortunately, due to some limitations of the plug-in manager, I have had to put some of the eBay Integration files in other locations. But, I have developed the plug-in so that it does not require modifying any of the Zen Cart core files or any of the existing database tables. This will make maintenance and upgrading easier and less risky.

The plug-in is “installed” in the Zen Cart zc_plugins folder in a folder called ebayintegration which contains a folder called v1.0.0 which, obviously, is the version of the plug-in . In that folder is a file called manifest.php which describes the plugin.

<?php

return [
    'pluginVersion' => 'v1.0.0',
    'pluginName' => 'eBay Integration',
    'pluginDescription' => 'eBay Integration for Zen Cart.',
    'pluginAuthor' => 'Roger Smith (sales@jonrocket.com)',
    'pluginId' => 1818, // ID from Zen Cart forum
    'zcVersions' => ['v157'],
    'changelog' => '', // online URL (eg github release tag page, or changelog file there) or local filename only, ie: changelog.txt (in same dir as this manifest file)
    'github_repo' => '', // url
    'pluginGroups' => [],
];
Code language: HTML, XML (xml)

Zen Cart uses the manifest to install and enable the plug-in through the Zen Cart admin Plug-in Manager. As you can see, it defines things like the name of the plug-in, developer, and version number.

Also in the v1.0.0 folder are two subfolders – installer and admin.

The installer folder contains a PHP file called ScriptedInstaller.php. As the name implies, this is code that installs the plug-in. It calls a method to register the eBay Integration administration page and add it to the Zen Cart Tools menu.

The admin folder contains subfolders that mirror the structure under the main Zen Cart admin folder (which you should have renamed). In the admin folder is the code for the eBay Integration administration page, ebayintegration.php.

Under the admin folder is an include folder. As mentioned earlier, this folder mirrors the admin/includes folder in the Zen Cart root folder. So, the includes folder contains folders that define the classes, CSS, images, language, and other stuff used by the plug-in.

I’m not sharing the code, yet, because it is still under construction.

Categories
eBay Integration

eBay Integration – the eBay API

Integration with eBay is made possible by the eBay Application Program Interface (API) which is a collection of services that expose methods that can be called from your server to access information and to define notifications to be sent by eBay to your server.

What this means is that the eBay Integration plug-in for Zen Cart, running on your server, can access your account data at eBay. It can create listings, get notified of orders, update listings, and more.

In order to do all these neat things, the plug-in has to get permission to access the API and your data. The plug-in accesses the API by making RESTfull calls to eBay’s servers using the same HTTP protocol that your browser uses to get a web page from a web site or submit a form to it.

In order to use the API, the plug-in needs permission to access it. eBay uses three identifiers as keys – sort like a user ID and password – to let your plug-in call the API methods. The keys are assigned to your application (the plug-in). To create the keys, you have to create an account at eBay’s Developer web site and register your application.

The keys give the plug-in permission to call the eBay API methods, but only to get public information. To get access to your eBay account information, the plug-in needs to get a security token.

The security token grants permission to the plug-in allowing it to access the account information that it needs.

To talk to the eBay API, the plug-in uses a slightly-updated version of a PHP class written by eBay. The eBaySessionHandler class (called just eBaySession by eBay) encapsulates the code for sending a request to an eBay server and receiving the response.

/**
 *  eBay's PHP code for making API calls
 */
class eBaySessionHandler
 {
  var $devID;
  var $appID;
  var $certID;
  var $serverUrl;
  var $compatLevel;
  var $siteID;
  var $verb;

  /**
   * __construct
   * Constructor to make a new instance of eBaySessionHandler with the details needed to make a call
   * Note that authentication credentials (normally token, but could be username and password)
   * are assumed to come in the request body, and not in the constructor args
   * Input: $developerID - Developer key obtained when registered at http://developer.ebay.com
   * $applicationID - Application key obtained when registered at http://developer.ebay.com
   * $certificateID - Certificate key obtained when registered at http://developer.ebay.com
   * $severUrl - URL of the server to use
   * $compatabilityLevel - API version this is compatable with
   * $siteToUseID - the Id of the eBay site to associate the call iwht (0 = US, 2 = Canada, 3 = UK, ...)
   * $callName - The name of the call being made (e.g. 'GeteBayOfficialTime')
   * Output: Response string returned by the server
   */
  function __construct($developerID, $applicationID, $certificateID, $serverUrl, $compatabilityLevel, $siteToUseID, $callName)
   {
    // $this->requestToken = $userRequestToken;
    $this->devID = $developerID;
    $this->appID = $applicationID;
    $this->certID = $certificateID;
    $this->compatLevel = $compatabilityLevel;
    $this->siteID = $siteToUseID;
    $this->verb = $callName;
    $this->serverUrl = $serverUrl;
   }

  /**
   * sendHttpRequest
   * Sends a HTTP request to the server for this session
   * Input: $requestBody
   * Output: The HTTP Response as a String
   */
  function sendHttpRequest($requestBody)
   {
    // build eBay headers using variables passed via constructor
    $headers = $this->buildEbayHeaders ();

    // initialise a CURL session
    $connection = curl_init ();
    // set the server we are using (could be Sandbox or Production server)
    curl_setopt ( $connection, CURLOPT_URL, $this->serverUrl );

    curl_setopt ( $connection, CURLOPT_TIMEOUT, 30 );

    // stop CURL from verifying the peer's certificate
    curl_setopt ( $connection, CURLOPT_SSL_VERIFYPEER, 0 );
    curl_setopt ( $connection, CURLOPT_SSL_VERIFYHOST, 0 );

    // curl_setopt($connection, CURLOPT_HEADER, 1 ); // Uncomment these for debugging
    // curl_setopt($connection, CURLOPT_VERBOSE, true); // Display communication with serve

    curl_setopt ( $connection, CURLOPT_TIMEOUT, 360 );

    // set the headers using the array of headers
    curl_setopt ( $connection, CURLOPT_HTTPHEADER, $headers );

    // set method as POST
    curl_setopt ( $connection, CURLOPT_POST, 1 );

    // set the XML body of the request
    curl_setopt ( $connection, CURLOPT_POSTFIELDS, $requestBody );

    // set it to return the transfer as a string from curl_exec
    curl_setopt ( $connection, CURLOPT_RETURNTRANSFER, 1 );

    // Send the Request
    $response = curl_exec ( $connection );

    // print curl_error($connection) . ": " . $response;

    // close the connection
    curl_close ( $connection );

    // return the response
    return $response;
   }

  /**
   * buildEbayHeaders
   * Generates an array of string to be used as the headers for the HTTP request to eBay
   * Output: String Array of Headers applicable for this call
   */
  function buildEbayHeaders()
   {
    $headers = array ( // Regulates versioning of the XML interface for the API
      'X-EBAY-API-COMPATIBILITY-LEVEL: ' . $this->compatLevel,

      // set the keys
      'X-EBAY-API-DEV-NAME: ' . $this->devID,
      'X-EBAY-API-APP-NAME: ' . $this->appID,
      'X-EBAY-API-CERT-NAME: ' . $this->certID,

      // the name of the call we are requesting
      'X-EBAY-API-CALL-NAME: ' . $this->verb,

      // SiteID must also be set in the Request's XML
      // SiteID = 0 (US) - UK = 3, Canada = 2, Australia = 15, ....
      // SiteID Indicates the eBay site to associate the call with
      'X-EBAY-API-SITEID: ' . $this->siteID
    );

    return $headers;
   }
 }
Code language: PHP (php)

I added classes to encapsulate handling of the specific eBay requests that the plug-in uses. All are derived from a base class I called eBayRequest.

/**
 *  An eBay request
 */
abstract class eBayRequest
 {
  private $requestType;

  /**
    *  @brief Constructor
    *   
    *  @param string $requestType the eBay API request type
    *  @return void
    */
  function __construct($requestType)
   {
    $this->requestType = $requestType;
   }

  /**
    *  @brief getXML - return the XML used to make the API the call
    *  
    *  @return string the XML of the call
    */
  abstract protected function getXML();

  /**
   *  @brief addResults - add results to the list of results
   *  
   *  @param string $xml the output XML from the API call
   *  @param array $results the array to hold the $results
   *  @return array the array of results
   */
  abstract protected function addResults($xml, $results);

  /**
   *  @brief getServerURL - return the URL of the API call
   *  
   *  @return sting the URL
   */
  protected function getServerURL()
   {
    return "https://api.ebay.com/ws/api.dll";
   }

  /**
   *  @brief getResults - make the actual API call and return the results
   *
   *  @param boolean|array - if true, error messages are not logged.  If an array, 
   *                         messages are logged only for errors not in the array 
   *                         of error codes.
   *  
   *  @return array the results
   */
  public function getResults($suppressErrors = false)
   {
    $devID = ebayGetConfiguration ( 'PROD_DEVID' );
    $appID = ebayGetConfiguration ( 'PROD_APPID' );
    $certID = ebayGetConfiguration ( 'PROD_CERTID' );
    $serverUrl = $this->getServerURL ();
    $compatabilityLevel = 705;
    $siteID = 0;
    $verb = $this->requestType;

    $session = new eBaySessionHandler ( $devID, $appID, $certID, $serverUrl, $compatabilityLevel, $siteID, $verb );

    $xml = $this->getXML ();
    
    if (function_exists('eBayModifyRequest'))
      {
        $xml = eBayModifyRequest($verb, $xml);
      }

    $response = $session->sendHttpRequest ( $xml );

    $results = array ();

    $results ['count'] = 0;
    $results ['errorcode'] = '0';
    $results ['errormessage'] = '';
    if (stristr ( $response, 'HTTP 404' ) || $response == '')
     {
      $results ['count'] = 0;
      $results ['status'] = 'Failure';
      $results ['errorcode'] = '999';
      $results ['errorlink']  = 'https://jonrocket.com/zencart/errorcode-' . $results ['errorcode'];
      $results ['errormessage'] = 'HTTP Error';
      
      $message = $verb . ': ' . $results ['errormessage'] . ' {<a href="' . $results['errorlink'] . '">' . $results ['errorcode'] . '</a>)';
      eBayLog($message, true);

      return $results;
     }
      
    $simpleXML = simplexml_load_string ( $response );

    $results ['status'] = ( string ) $simpleXML->Ack;

    if ($results ['status'] != 'Success')
     {
      $results ['errors'] = array ();
      foreach ( $simpleXML->Errors as $error )
       {
        $results ['errorcode'] = ( string ) $error->ErrorCode;
        $results ['errorlink']  = 'https://jonrocket.com/zencart/errorcode-' . $results ['errorcode'];
        $results ['errormessage'] = ( string ) $error->LongMessage;
        
        if (!$suppressErrors) 
          {
            $message = $verb . ': ' . htmlentities($error->LongMessage) . ' {<a href="' . $results ['errorlink'] . '">' . $error->ErrorCode . '</a>)';
            eBayLog($message, true);
          }
        
        
        $results ['errors'] [] = array (
          'errorcode' => ( string ) $error->ErrorCode,
          'errormessage' => $error->LongMessage,
          'errorlink' => $results['errorlink']
        );
       }
      return $results;
     }

    $results = $this->addResults ( $simpleXML, $results );

    return $results;
   }
 }
 
Code language: PHP (php)

The code for a specific request is derived from the eBayRequest class. For example, to get your recent eBay orders, a GetOrders request is made to eBay’s Trading API service.

/**
 *  GetOrders Request
 *  
 *  As a side-effect, executing this call will update the store
 *  quantities based on items sold on eBay.
 */
class eBayGetOrdersRequest extends eBayRequest
 {
  var $from;
  var $to;
  var $role;

  function __construct($from, $to, $role)
   {
    $this->from = eBayGetUTC8601 ( $from );
    $this->to = eBayGetUTC8601 ( $to );
    $this->role = $role;

    parent::__construct ( 'GetOrders' );
   }

  protected function getXML()
   {
    $authToken = ebayGetConfiguration ( 'AUTH_TOKEN' );

    $request = <<<XML
    
           <?xml version="1.0" encoding="utf-8" ?>
           <GetOrdersRequest xmlns="urn:ebay:apis:eBLBaseComponents">
             <RequesterCredentials>
                <eBayAuthToken>$authToken</eBayAuthToken>
             </RequesterCredentials>
             <CreateTimeFrom>{$this->from}</CreateTimeFrom>
             <CreateTimeTo>{$this->to}</CreateTimeTo>
             <OrderRole>{$this->role}</OrderRole>
             <OrderStatus>Completed</OrderStatus>
             <DetailLevel>ReturnAll</DetailLevel>
           </GetOrdersRequest>
    
XML;

    return $request;
   }

  function addResults($simpleXML, $results)
   {
    $results ['orders'] = array ();
    foreach ( $simpleXML->OrderArray->Order as $order )
     {
      $result = array ();
      $result ['orderid'] = ( string ) $order->OrderID;
      $result ['subtotal'] = floatval ( ( string ) $order->Subtotal );
      $result ['total'] = floatval ( ( string ) $order->Total );
      $result ['buyerid'] = ( string ) $order->BuyerUserID;
      $result ['paidtime'] = date_create ( ( string ) $order->PaidTime );
      $results ['orders'] [] = $result;
      foreach ( $order->TransactionArray->Transaction as $transaction )
       {
         $itemId = $transaction->Item->ItemID;
         $quantity = $transaction->QuantityPurchased;
         $transactionId = $transaction->TransactionID;
         eBayProcessItemSold($itemId, $transactionId, $quantity);
       }
     }
    $results ['count'] = count ( $results ['orders'] );

    return $results;
   }
 }
Code language: PHP (php)

The GetOrders request, like all of the eBay requests starts with creating XML which contains the information to send to eBay’s server. This includes the three IDs used as keys and the authorization token allowing access to the eBay store information.

The XML is sent to eBay’s server using the HTTP protocol. The eBay server returns a response in XML. The GetOrdersRequest class parses the returned XML to get information about the orders.

Categories
eBay Integration

eBay Integration – Integrating eBay and Zen Cart is a Challenge

You have a choice of many commercial plug-ins for Zen Cart which offer integration with eBay (and other marketplaces like Amazon and Wal-Mart). These tend to be expensive and many charge a fee per listing or sale. This is understandable because eBay integration is hard!

Making Zen Cart and eBay work together can be a challenge. This is mainly due to the number of options offered by eBay.

To reduce the challenge, my version of eBay Integration is mostly meant to satisfy our specific needs and not compete with the more complete solutions offered by others.

The initial version of my eBay Integration support for Zen Cart plug-in only works with the US version of eBay. Adding support for other countries shouldn’t be too difficult, but we only have an eBay store on the US site, so it is most important that we get that working first.

The other big limitation, at least for now, is that variations (size, color, etc.) won’t be handled. It’s not really the right way to do things, but we create separate products for each variation in Zen Cart and eBay.

A minor limitation will be that adding of products to eBay won’t be fully automated. The tool will create eBay listings from your Zen Cart products, but you will have to edit them (once!) in eBay before activating them in the tool. This is so that the tool doesn’t have to support all of the shipping options and business policies offered by eBay.

Finally, I am going to require you to get your own Developer Tokens for accessing eBay’s Application Program Interface (API). This saves me from having to get the plug-in approved by eBay while allowing you to update or modify it to support your specific needs.

Categories
eBay Integration

eBay Integration – Integrating Your Zen Cart Store and eBay Store

One of the first things I did after creating our online Zen Cart store was to write code to integrate it with our Ebay store. The code adds new listings to eBay when we add new products to our Zen Cart store. Then it keeps the eBay and Zen Cart inventories in sync as products are sold and restocked.

The original code was written quickly for Zen Cart 1.3.9a and served us well for many years. But when we finally upgraded to version 1.5.7a/b I decided to take a bit more time to make the code more maintainable and presentable.

This post is a first in a series describing the eBay Integration for Zen Cart project. The code is not quite ready to share yet, but when it is, I will offer it as a free download.