Using a Perl script because refactoring of a project with Android tools didn’t work

Posted on 20. Dezember 2014 Comments

I wanted to rename my project but I guess since it has a lot of dependencies that caused an error somewhere and I got the error message:

Refactoring
A fatal error occurred while performing the refactoring.
An unexpected exception occurred while creating a change object. See the error log for more details.

So I just the normal refactoring feature of Eclipse which not surprisingly also caused an error. After editing the AndroidManifest package entry, the import of the resources in the sources files didn’t work. It still said

import com.example.oldpackage.R

Only a couple of resource files needed manual editing but the Java files were a problem. What did the trick for me was this one-liner

perl -pi -w -e 's/import com.example.oldpackage.R/import com.example.newpackage.R/g;' `grep -r -l "import com.example.oldpackage.R"`

I realise this is rather quick&dirty (do a backup ;) ) but it did work for this project. A short explanation:

perl

  • -pi puts the code in a loop (like -n, but sed-style)
  • -w gives you warnings
  • -e is one line of programm and since -pi
  • /g global, for all lines in the file

grep

  • -r recursive
  • -l give out files that matches the following search string

flattr this!

How to change nano syntax highlighting for arbitrary filename extensions

Posted on 17. Dezember 2014 Comments

The text editor nano manages syntax highlighting through .nanorc files. Say, we have files with the ending .phpx, but would like PHP syntax highlighting.

$ sudo nano /usr/share/nano/php.nanorc

Change this line

syntax "php" "\.php[2345s~]?$"

to this regex:

syntax "php" "\.php[x2345s~]?$".

Alternativly, e.g. if you don’t have sudo rights for /usr/share/nano/php.nanorc, you can do the following

$cp /usr/share/nano/php.nanorc ~/.phpx.nanorc

Then enter the following line in ~/.nanorc:

include ~/.phpx.nanorc

Then, change the line as mentioned above and syntax “php” into syntax “phpx”

flattr this!

Automize selling at LaRedoute #2: Parse order files

Posted on 16. Dezember 2014 Comments

This blog post is part of the series Automize selling at LaRedoute.

  • Part 1: Get new orders
  • Part 2: Parse order files
  • Part 3: Upload response files
  • Part 4: update quantity and price feed

In part 1 the files containing orders are downloaded into a folder called OrdersFromLaRedoute. The next script is going to go through that folder, parse the file and insert it into a table.

These are the values the table must have because we’re just going to insert everything that’s in the CSV files.


$dbValues = array('MarketplaceID', 'OrderID', 'StorefrontOrderID', 'OrderDate', 'BuyerEmailAddress', 'BuyerName', 'BuyerPhoneNumber', 'OrderItemCode', 'ItemStatus', 'SKU', 'Title', 'Quantity', 'ItemPrice', 'ItemTax', 'ShippingCharge', 'ShippingTax','ItemFee', 'Currency', 'ShippingOption', 'PaymentInfo', 'ShippingAddressName', 'ShippingAddressFieldOne', 'ShippingAddressFieldTwo', 'ShippingAddressFieldThree', 'ShippingCity', 'ShippingStateOrRegion', 'ShippingPostalCode', 'ShippingCountryCode', 'ShippingPhoneNumber', 'BillingAddressName', 'BillingAddressFieldOne', 'BillingAddressFieldTwo', 'BillingAddressFieldThree', 'BillingCity', 'BillingStateOrRegion', 'BillingPostalCode', 'BillingCountryCode', 'BillingPhoneNumber');

 

Therefore I created the table more or less like this:

CREATE TABLE `ORDERS-HISTORY` (
`MarketplaceID` int(11) DEFAULT NULL,
`OrderID` varchar(50) DEFAULT NULL,
`StorefrontOrderID` varchar(50) DEFAULT NULL,
`OrderDate` varchar(50) DEFAULT NULL,
`BuyerEmailAddress` varchar(50) DEFAULT NULL,
`BuyerName` varchar(50) DEFAULT NULL,
`BuyerPhoneNumber` varchar(50) DEFAULT NULL,
`OrderItemCode` varchar(50) NOT NULL DEFAULT '',
`ItemStatus` varchar(10) NOT NULL DEFAULT '',
`SKU` int(11) DEFAULT NULL,
`Title` varchar(50) DEFAULT NULL,
`Quantity` int(11) DEFAULT NULL,
`ItemPrice` decimal(8,2) DEFAULT NULL,
`ItemTax` decimal(8,2) DEFAULT NULL,
`ShippingCharge` decimal(8,2) DEFAULT NULL,
`ShippingTax` decimal(8,2) DEFAULT NULL,
`ItemFee` decimal(8,2) DEFAULT NULL,
`Currency` varchar(3) DEFAULT NULL,
`ShippingOption` varchar(10) DEFAULT NULL,
`PaymentInfo` varchar(50) DEFAULT NULL,
`ShippingAddressName` varchar(70) DEFAULT NULL,
`ShippingAddressFieldOne` varchar(70) DEFAULT NULL,
`ShippingAddressFieldTwo` varchar(70) DEFAULT NULL,
`ShippingAddressFieldThree` varchar(70) DEFAULT NULL,
`ShippingCity` varchar(70) DEFAULT NULL,
`ShippingStateOrRegion` varchar(70) DEFAULT NULL,
`ShippingPostalCode` int(11) DEFAULT NULL,
`ShippingCountryCode` varchar(3) DEFAULT NULL,
`ShippingPhoneNumber` varchar(50) DEFAULT NULL,
`BillingAddressName` varchar(70) DEFAULT NULL,
`BillingAddressFieldOne` varchar(70) DEFAULT NULL,
`BillingAddressFieldTwo` varchar(70) DEFAULT NULL,
`BillingAddressFieldThree` varchar(70) DEFAULT NULL,
`BillingCity` varchar(70) DEFAULT NULL,
`BillingStateOrRegion` varchar(70) DEFAULT NULL,
`BillingPostalCode` int(11) DEFAULT NULL,
`BillingCountryCode` varchar(3) DEFAULT NULL,
`BillingPhoneNumber` varchar(50) DEFAULT NULL,
PRIMARY KEY (`OrderID`,`OrderItemCode`,`ItemStatus`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

 

As you can see, the primary key consists of OrderID, OrderItemCode and ItemStatus. An OrderID is the unique identifier of one order, consisting of possibly many but at least one OrderItemCode. An OrderItemCode is representing an SKU + Quantity. For newly created orders, the ItemStatus will appear as “Created”. Once an item is accepted, LaRedoute will put a file into the ToSupplier folder with exactly the same OrderID, OrderItemCode but the ItemStatus “ToShip”. This will be important in Step 3.

This will give you an array of files downloaded in part 1:


$dir = "OrdersFromLaRedoute";
$dirAsArray = scandir("OrdersFromLaRedoute");

 

This loop takes each file (“.” and “..” are there because it’s a directory listing) and opens the file. If the current line includes the word “BuyerName” this is used as in indication that the current line is the header. The loop will then continue with the next item. Otherwise the $csvLine variable can be inserted into a database table. Because this script could run as e.g. a cron job, moving the files is necessary, in this case to an archive folder.


foreach($dirAsArray as $file) {
if (!($file === "." || $file === "..")) {
if (($handle = fopen($dir . "/" . $file, "r")) !== FALSE) {
while (($csvLine = fgetcsv($handle, 5000, "\t")) !== FALSE) {
if (in_array("BuyerName",$csvLine)) {
continue;
}
// insert $csvLine into table
}
fclose($handle);
}
rename($dir . "/" . $file, 'OrdersFromLaRedoute/archive/' . $file);
}
}

 

flattr this!

Automize selling at LaRedoute #1: Get new orders

Posted on 16. Dezember 2014 Comments

This blog post is part of the series Automize selling at LaRedoute.


The french marketplace LaRedoute unfortunately doesn’t have a real API, but they do have ways to automize some processes. A lot of smaller marketplaces have this concept as well. You will get credentials for an SFTP server. On this server you will find the folders ToSupplier and FromSupplier, where the “supplier” (aka you) can up- and download a range files documented by Merchantry in their blog. The processing of the uploaded files can take up to 6 hours, but is sometimes done in only a couple of minutes, so I’m going to assume the worst case of 6 hours in this post.

While programming a couple of scripts I found the following problems:

  • the server is incredibly slow sometimes (better at nights), so sometimes the connections just time out
  • sometimes the listing for the ToSupplier folder times out because there are too many files (according to support…huh?), so they have to be deleted regularly
  • not only the connection to LaRedoute but also the connection to my local MySQL server times out
  • I have to reserve a purchased item once I accepted it on LaRedoute immediately, because it could be sold elsewhere in the 6 hours LaRedoute might take to give me the shipping address

New orders can be found in the ToSupplier folder in tab seperated CSV files (but .txt ending) with the format OrdersYYYY-MM-DD-hh-mm-ss.txt.

Since PHP is the companies main language I will show a couple of scripts which automize downloading and processing those files. The code is of course simplified for better understanding. We’re using SFTP instead of FTP and I found using the phpseclib to be the most usable library.

I will propose the use of 2 Tables in the MySQL database: TEMP-FILENAMES and FILENAMES-HISTORY. Both have a the unique column filename. FILENAMES-HISTORY will contain the name of every file ever processed by the following script, TEMP-FILENAMES is a helper table that will be truncated after every run.

First, we need to establish a connection


$sftp = new Net_SFTP(SFTP_LAREDOUTE_HOST);
if (!$sftp->login(SFTP_LAREDOUTE_USER, SFTP_LAREDOUTE_PASS)) {
exit('Login Failed');
}

Then we change directory. This is a command that usually involves listing the directory changed into, but since this is not a graphical client, the real timeout might come on line below. $nlist will just be null if the listing fails, and I will assume it didn’t work if it takes more than 30 seconds.

$sftp->chdir('/ToSupplier');

$beforetime = time();
$nlist = $sftp->nlist();
$aftertime = time();
if(($aftertime-$beforetime) > 30 ) {
exit('Timeout while Listing directory');
}

The next piece of code is only executed if the listing worked. Every filename that includes the word “Order” is now inserted into the temporary table:

foreach($nlist as $filename) {
if (strpos($filename, 'Order') !== false) {
$qry = "INSERT INTO `TEMP-FILENAMES`(`filename`) VALUES ('". $filename . "')";
$insert = mysql_query($qry,MYSQLCONNECTION) or print mysql_error();
}
}

You can look at the difference between the filenames in your HISTORY table and the possibly new ones in the temporary table.

$tmpCmpFilenames = array();
$qry = "SELECT `filename` FROM `TEMP-FILENAMES` WHERE `filename` NOT IN (SELECT `filename` FROM `FILENAMES-HISTORY`)";
$select = mysql_query($qry, MYSQLCONNECTION) or print mysql_error();
while ($row = mysql_fetch_assoc($select)) {
$tmpCmpFilenames[] = $row['filename-id'];
}

Now we have all the new files in the array $tmpCmpFilenames. The correct way would be make sure the downloaded files are correct with hashes. Instead we decided to misuse the filesize, since it’s a good indicator something didn’t work properly;) The files not downloaded correctly are deleted from the array. They will appear next time the script is run.

foreach($tmpCmpFilenames as $filename) {
$remotefilesize = $sftp->size($filename);
$sftp->get($filename, 'OrdersFromLaRedoute/' . $filename);
$localfilesize = filesize('OrdersFromLaRedoute/' . $filename);
if ($remotefilesize != $localfilesize) {
unset($tmpCmpFilenames[$filename]);
}
}

We can now insert the filenames into the HISTORY table.

foreach($tmpCmpFilenames as $filename) {
$qry = "INSERT INTO `FILENAMES-HISTORY`(`filename`) VALUES ('". $filename . "')";
$insert = mysql_query($qry, MYSQLCONNECTION) or print mysql_error();
}

Last but not least, the temporary table needs to be truncated for the next run.

$truncate=mysql_query("TRUNCATE TABLE `TEMP-FILENAMES`",MYSQLCONNECTION) or print mysql_error();

The next step is described in part 2 of this series.

flattr this!

Minimal Mensa Plan

Posted on 9. Dezember 2014 Comments

We’ve been doing a lot of website scraping for a university project latel so I decided a little app to scrape the universitys’ cantine website for the lunch menu.

minimalmensaplan-screenshot

A network connection in the main Activity is not allowed so I’m using a private class for that. Once the doInBackground method is over onPostExecute is automatically called.

Luckily the jSoup library which is used for parsing also brings a way download a website for parsing.

Document doc = Jsoup.connect("http://speiseplan.studierendenwerk-hamburg.de/de/520/2014/0/").get();

There is only one category class and it contains the date. The dish-description class contains what you will later see in the app. The size is needed for a for-loop later on.

date = doc.select(".category").text();
int maxDishes = doc.getElementsByClass("dish-description").size();

With the next piece of code all the dish descriptions are extracted. The original website contains details about the food (made with alcohol, pork or if it’s vegeterian etc). For blending this out I use a regular expression which filters out:

1 non-word character(\W = an opening bracket), then possibly multiple digits (\d) and non-word characters (\W = commas) and then another non-word character (\W = a closing bracket)

String dish = doc.getElementsByClass("dish-description").get(i).text().replaceAll("(\\W[\\d\\W]*\\W)", " ");

These dishes are then saved together with every second price (the one for students, the other one’s for employees) in a HashMap, which is then added to a list. For recognizing later, the dishes get the key “dish” and the prices the key “price”.

Once the data extraction is done, onPostExecute is automatically called. The date is set to a TextView above the ListView and a SimpleAdapter is populating the list of HashMaps into the layout

dateTextView.setText(date);
simpleAdapter = new SimpleAdapter(MainActivity.this,
dishList, R.layout.list, new String[] { "dish",
"price" }, new int[] { R.id.text1, R.id.text2 });
setListAdapter(simpleAdapter);

Since i’s called Minimal Mensa Plan, no other features (such as caching or selecting a cantine) are available. The app in the Play Store is used for scraping the cantine at the campus Berliner Tor but it might as well be used for others, just by changing the URL. It’s released under the MIT License and available at GitHub.

flattr this!

WordPress cache mit redis und uberspace

Posted on 24. Oktober 2014 Comments

Redis ist eine In-Memory Key-Value Datenbank. Das heißt, dass die Daten im Arbeitsspeicher gehalten werden und deswegen besonders schnell verfügbar sind. Dafür sind sie aber auch nicht persistent gespeichert, d.h. bei einem Neustart sind die Daten weg. Das ist aber im Grunde perfekt für einen Cache Speicher.

WordPress ist ja nicht gerade für seine Schnelligkeit bekannt und so gibt es diverse Plugins zum besseren Cachen. Seine eigene Seite kann man z.B. bei Google Page Speed testen. Jedes Mal wenn eine Seite abgerufen wird, muss PHP Daten aus der Datenbank holen und eine Seite generieren. Je nach Hoster, Anbindung etc kann das ganz schön lange dauern.

Die Idee für diesen Post ist Daten, die sich ohnehin nicht so häufig ändern, in einer Redis Datenbank vorzuhalten und statt jedes Mal eine Seite generieren zu lassen einfach diese Daten aus dem Redis Cache abzufragen. Dazu braucht man aber eine Verbindung zwischen PHP und Redis und überhaupt die Möglickeit Redis auf demselben Webserver zu installieren auf dem auch WordPress liegt. Leider bieten das nicht allzu viele Anbieter, mein Anbieter Uberspace jedoch schon. PHP kann über die Bibliotheken Predis oder  PHPRedis auf Redis Server zugreifen.

Das Plugin wp-redis-cache übernimmt jegliche Arbeit. Zum Installieren muss man einfach nur der Anleitung folgen. Die IP seines eigenen Webservers bekommt man z.B. über Ping. Bei Uberspace läuft Redis nicht über TCP sondern über einen Socket. Dieser befindet sich im Home-Verzeichnis ~/.redis/sock.

Als Erstes muss man auf seinem Uberspace Redis installieren:

$ test -d ~/service || uberspace-setup-svscan
$ uberspace-setup-redis

Wenn man statt dem mitgelieferten Predis 5.2 lieber PHPRedis benutzen möchte kann man dies folgendermaßen installieren:

$ uberspace-install-pecl redis

Mit meinem Patch für wp-redis-cache kann man nun auch Sockets mit Predis nutzen.
Wichtig ist nun noch die Variable $redis_server auf folgenden Wert zu setzen.

/home/username/.redis/sock

Die Abkürzung ~/.redis funktioniert hier nicht.

Solange die Variable $debug noch auf true steht, kann man beim Aufruf der Website im Quelltext die gemessene Zeit bis zur Ausgabe sehen. Diese sollte deutlich unter vorherigen Werten liegen.

Im WordPress Backend kann man unter Einstellungen/Wp Redis Cache noch eine maximale Zeit (in Sekunden) angeben, in denen der Cache geleert und neu befüllt wird, um ggf. Änderungen anzuzeigen. Zum Testen kann man aber auch einen $secret_key in der index-wp-redis.php festlegen und seinen Blog so aufrufen: http://blog-url.tld/?refresh=refreshpasswort
Oder man startet einfach Redis neu:

$ svc -du ~/service/redis

flattr this!

Anleitung: Produkte bei eBay über API mit PHP SDK listen – Teil 4: Aufträge löschen, Artikel aktualisieren

Posted on 13. Oktober 2014 Comments

Dieser Blog Post ist Teil der Reihe Produkte bei eBay listen.


4.1. Jobs abbrechen

Es kann immer mal vorkommen, dass es bei der Feed-Erstellung Fehler gibt. Dann wird ggf. von large-merchant-services/02-add-fixed-price-item.php ein Job erstellt. Diesen muss man erst abbrechen um einen neuen Job derselben Art zu erstellen. Dazu braucht man die jobID. Normalerweise wird diese beim Erstellen des Jobs mit angezeigt. Sollte man sie aus irgendeinem Grund nicht zur Verfügung haben, kann man sie aber auch mit large-merchant-services/01-get-jobs.php erfragen. Da hier aber alle Jobs, die jemals ausgeführt wurden, angezeigt werden, muss man noch ein paar kleine Änderungen vornehmen. Als erstes sollte man aber wie auch in Teil 3 sicherstellen, dass man auf der richtigen Plattform arbeitet (hier: Sandbox):

'authToken' => $config['sandbox']['userToken'],
'sandbox' => true

Weiter unten, in der letzten if-Abfrage, sollte man die folgende Line ändern:

$upTo = min(count($response->jobProfile), 500);

Das sollte erstmal reichen. Außerdem kann folgende Zeile hinzufügen

$job = $response->jobProfile[$x]; // alte Zeile (Z. 89 bei mir)
if ($job->jobStatus != "Completed") { // das hier einfügen
...
} // if-Abfrage weiter unten schließen

Jetzt bekommt man nur die Jobs, die noch nicht fertig gestellt sind. Allerdings sind hier auch die Abgebrochenen dabei, also könnte man auch  ein != “Aborted” hinzufügen.

Hat man nun die jobID, kann man die Vorlage large-merchant-services/01-get-jobs.php ein bisschen abändern. Ich habe die Datei large-merchant-services/04-abort-job.php genannt. Dafür muss in Zeile 58 den richtigen Request und die jobID eingetragen.

$request = new Types\AbortJobRequest(array('jobId' => '4711'));

Eine Zeile darunter:

$response = $service->abortJob($request);

Sollte man das öfter brauchen, sollte es auch nicht allzu schwer sein, die jobID aus den Parametern zu ziehen.

4.2. Preise und Verfügbarkeit aktualisieren

Wie viele andere Marktplätze, gibt eBay auch die Möglichkeit mit einem reduzierten Feed für zuvor gelistete Produkte die Verfügbarkeit und Preise zu updaten. Eine genaue Erklärung der einzelnen Tags findet sich im ersten Teil. Der reduzierte Feed folgendermaßen aus:

<?xml version="1.0" encoding="UTF-8"?>
<BulkDataExchangeRequests>
<Header>
<Version>669</Version>
<SiteID>77</SiteID>
</Header>
<ReviseFixedPriceItemRequest xmlns="urn:ebay:apis:eBLBaseComponents">
<ErrorLanguage>de_DE</ErrorLanguage>
<WarningLevel>Low</WarningLevel>
<Version>619</Version>

Jetzt kommen die Informationen das Item, also der Eltern-/Parent-Artikel

<Item>
<SKU>26754</SKU>
<InventoryTrackingMethod>SKU</InventoryTrackingMethod>

Mehr ist nicht von nöten, da die Verfügbarkeiten und Preise ja auf Variationsebene festgelegt sind.

<Variations>
<Variation>
<SKU>4711</SKU>
<Quantity>23</Quantity>
<StartPrice>12.34</StartPrice>
</Variation>
<Variation>
<SKU>4712</SKU>
<Quantity>42</Quantity>
<StartPrice>56.78</StartPrice>
</Variation>
... <!-- mehr Variationen hier -->
</Variations>
</Item>
</ReviseFixedPriceItemRequest>
</BulkDataExchangeRequests>

Das war es auch schon. Für das Updaten der Angebote (revise) gibt es noch keine Vorlage von dts. Allerdings kann man das Beispiel für AddFixedPriceItem mit wenigen Änderungen auch dafür benutzen. In Zeile 75 wird statt AddFixedPriceItem einfach ReviseFixedPriceItem eingesetzt:

$createUploadJobRequest->uploadJobType = 'ReviseFixedPriceItem';

Außerdem ändern sich natürlich noch die Dateinamen der hochzuladenen Datei und der Antwort von eBay:

$uploadFileRequest->attachment(file_get_contents(__DIR__.'/ReviseFixedPriceItem.xml.gz'));
...
$tempFilename = tempnam(sys_get_temp_dir(), 'revise-fixed-price-item-responses-').'.zip';

Natürlich kann man auch jede beliebige Änderung über ReviseFixedPriceItem vornehmen. Allerdings funktioniert alles außer Verfügbarkeit und Preis nur, solange noch keine Variation des Artikels gekauft wurde. Manchmal kann es auch gut sein, den Artikel einfach ganz zu löschen und noch einmal neu hochzuladen (Achtung: in Production kann das Geld kosten!). Dafür braucht man den folgenden API Call.

4.3. EndFixedPriceItem

Möchte man ein Angebot komplett löschen, kann man einen einfachen Feed für diese Items schreiben (für Varianten reicht es die Quantity auf 0 zu setzen). Ist bei allen Varianten die Quantity auf 0, wird der Artikel automatisch entfernt (und muss ggf. mit kostenpflichtig wieder erstellt werden), außer man hat beim Einstellen, wie in Teil 1 erwähnt, die Option OutOfStockControl gesetzt. In diesem Fall wird der Artikel bloß ausgeblendet. Die SKU ist hier dementsprechend die Eltern-/Parent-SKU. Der Feed sieht wie folgt aus:

<?xml version="1.0" encoding="utf-8"?>
<BulkDataExchangeRequests xmlns="urn:ebay:apis:eBLBaseComponents">
<Header>
<Version>659</Version>
<SiteID>77</SiteID>
</Header>
<EndFixedPriceItemRequest xmlns="urn:ebay:apis:eBLBaseComponents">
<EndingReason>NotAvailable</EndingReason>
<SKU>4711</SKU>
<ErrorLanguage>de_DE</ErrorLanguage>
<WarningLevel>High</WarningLevel>
<Version>859</Version>
</EndFixedPriceItemRequest>
...
<!-- Mehr EndFixedPriceItemRequests -->
</BulkDataExchangeRequests >

Das Prinzip ist dasselbe wie bei AddFixedPriceItem und ReviseFixedPriceItem: einfach die Strings im Quellcode ändern und man kann die Datei auch für EndFixedPriceItem benutzen.

flattr this!

Anleitung: Produkte bei eBay über API mit PHP SDK listen – Teil 3: Keys, Sandbox und AddFixedPriceItem

Posted on 13. Oktober 2014 Comments

Dieser Blog Post ist Teil der Reihe Produkte bei eBay listen.


3.1/3.2. Keys und Sandbox

Um die Beispiele von dts ausführen zu können muss man sich am eBay Developer Program anmelden. Man kann hier nicht seinen normalen eBay Account benutzen. Nach der Anmeldung kann man sich jeweils eine DEVID, AppID und CertID für Sandbox und Production erstellen. Erstmal ist es sicher sinnvoll Sandbox Keys zu erstellen. Anschließend musst man sich noch einen User Token generieren lassen. Die mit den Sandbox Keys eingestellten Angebote wird man später unter sandbox.ebay.com bzw. in der deutschen Version finden.

3.3. Vorbereitung des SDK

Abhängigkeiten installieren:

git clone https://github.com/davidtsadler/ebay-sdk-examples.git
curl -sS https://getcomposer.org/installer | php
php composer.phar install

Jetzt müssen nur noch die eben erstellen Keys und der Usertoken in die configuration.php eingetragen werden. Das eigentliche SDK befindet sich in vendor/dts, man muss also nichts anderes von GitHub herunterladen.

3.4. AddFixedPriceItem

Die benötigten Dateien befindet sich im Ordner large-merchant-services, für das Hinzufügen von Artikel wird die 02-add-fixed-price-item.php gebraucht. Sie ist relativ gut verständlich geschrieben. Man sollte darauf achten, dass in den Variablen $exchangeService und $transferService die Sandbox Keys benutzt werden:

'authToken' => $config['sandbox']['userToken'],
'sandbox' => true

Man kann diese Datei mehr oder weniger so benutzen. Sie wird eine .gz-gezippte Datei namens AddFixedPriceItem.xml.gz aus demselben Ordner hochladen. Wenn die .xml-Datei bzw. .gz-Datei wie in Teil 1 und 2 beschrieben erstellt ist und im selben Ordner liegt, kann diese .php-Datei einfach ausgeführt werden. Das Hochladen bzw. Verarbeiten seitens eBay kann eine ganze Weile dauern. Es wird auf jeden Fall eine .xml-Datei gezippt zurückkommen. In ihr findet man ggf. Fehlermeldungen, Warnings und was das Erstellen gekostet hat. In der Sandbox ist dies natürlich kostenlos, es werden 0.0 € berechnet. Es ist deshalb, vor allem bei größeren Tests, wichtig noch einmal die Keys zu überprüfen, so dass nicht ungewollt Kosten entstehen. Möchte man irgendwann umstellen, sehen die beiden Variablen oben folgendermaßen aus:

'authToken' => $config['production']['userToken'],
'sandbox' => false

Außerdem müssen dann in der configuration.php die entsprechenden keys bzw. ein neuer Usertoken für die Production Keys eingetragen sein.

Wird die Datei ausgeführt, die AddFixedPriceItem.xml.gz liegt aber nicht im gleichen Verzeichnis oder wird das Hochladen abgebrochen ist trotzdem ein Job erstellt. Ein weiterer Job des Typs AddFixedPriceItem ist nicht möglich und so muss erst der Job abgebrochen werden. Zu diesen und weiteren nützlichen API, siehe Teil 4.

flattr this!

Anleitung: Produkte bei eBay über API mit PHP SDK listen – Teil 2: XML Dateien mit PHP erstellen

Posted on 10. Oktober 2014 Comments

Dieser Blog Post ist Teil der Reihe Produkte bei eBay listen.


2.1. XMLWriter

Für das Schreiben von XML in PHP wird hier die XMLWriter Klasse verwendet, sie sollte eigentlich überall vorhanden sein. Als erstes wird ein Objekt erstellt.

$writer = new XMLWriter();

Mit dem folgenden Code kann man zwischen Ausgabe im Browser/auf der Konsole und dem Schreiben in eine .xml-Datei hin- und herschalten.

if ($DEBUG) {
$writer->openURI('php://output');
} else {
$filename = 'AddFixedPriceItem.xml';
touch($filename);
$writer->openURI($filename);
}

Der XMLWriter macht allerdings keine Absätze und so würden die folgende Anweisungen alles in eine Zeile schreiben. Prinzipiell ist das natürlich erstmal nicht unbedingt ein Problem. Allerdings wird eBay die Datei ab einer bestimmten Zeilenlänge nicht mehr akzeptieren, (wahrscheinlich) da die Zeilenanzahl ein Kriterium für die maximale Größe von BulkDataExchangeRequests sind.

$writer->setIndent(true);

Bevor die Elemente geschrieben werden, wird erst einmal das Dokument mit Version und Encoding begonnen.

$writer->startDocument('1.0', 'UTF-8');

Ergebnis:

<?xml version="1.0" encoding="UTF-8"?>

Ab jetzt können beliebig Elemente geschrieben werden. Übergeordnete Elemente können mit den folgenden Befehlen geöffnet und geschlossen werden. Dabei ist beim Schließen der Name egal, es zählt die Reihenfolge. Gerade in Schleifen sollte man hier also genau hinsehen.

$writer->startElement('BulkDataExchangeRequests');
...
$writer->endElement();

Ergebnis:

<BulkDataExchangeRequest>
...
</BulkDataExchangeRequest>

Soll ein Element mit einem Wert geschrieben werden wird der folgende Befehl verwendet

$writer->writeElement('SiteID', '77');

Ergebnis

<SiteID>77</SiteID>

Nun gibt es noch den seltenen Fall, dass in einem Tag noch ein Attribut vorhanden ist. Dies wird folgendermaßen realisiert:

$writer->startElement('ShippingServiceCost');
$writer->writeAttribute('currency', 'EUR');
$writer->text('0.0');
$writer->endElement();

Ergebnis:

<ShippingServiceCost currency="EUR">0.0</ShippingServiceCost>

Zuletzt sollte das Dokument noch geschlossen und der Puffer geschrieben (entweder in die Ausgabe oder in die Datei) werden:

$writer->endDocument();
$writer->flush();

2.2. Dateien zippen

In den Beispielen, die in Teil 3 benutzt werden, wird die .xml-Datei noch komprimiert, bevor sie hochgeladen wird. Dies kann mit dem folgenden Snippet umgesetzt werden.

if (!$DEBUG) {
$gzfile = $filename . ".gz";
$fp = gzopen($gzfile, 'w9');
gzwrite($fp, file_get_contents($filename));
gzclose($fp);
}

flattr this!

Anleitung: Produkte bei eBay über API mit PHP SDK listen – Teil 1: XML Feed

Posted on 10. Oktober 2014 Comments

Dieser Blog Post ist Teil der Reihe Produkte bei eBay listen.


Wer eine große Menge an Produkten über eBay verkaufen möchte, kann entweder .csv Dateien ausfüllen oder die eBay API benutzen. Dafür stellt eBay offizielle SDKs für Java, .NET und Python zur Verfügung, wobei Python am vollständigsten ist. Der Entwickler David T. Sadler aka dts hat auf GitHub ein PHP SDK zur Verfügung gestellt welches viele Funktionen der API unterstüzt.

Für diesen Blogpost gehe ich davon aus, dass Produkte zu einem festen Preis (keine Auktionen) und in verschiedenen Varianten eines Typs (z.B. Farbe/Größe) verkauft werden sollen. Außerdem soll die BulkDataExchange API, ein Teil des LargeMerchantService, benutzt werden, um gleich mehrere tausend Produkte gleichzeitig listen zu können. Es soll weiterhin die sog. eBay Garantie erreicht werden, einer Art Stempel von eBay für Top-Seller mit den besten Konditionen für Kunden.

Diese Blogpost-Reihe wird folgende Themen behandeln

  • Erstellen eines XML Feeds
  • Anmelden für das Developer Programm und nutzen der Sandbox
  • Initiales Listen einer großen Anzahl von Produkten
  • Regelmäßiges Updaten der Verfügbarkeit (Quantity) und der Preise
  • Löschen von Produktlistings

1. Erstellen eines XML Feeds

1.1. Header

Wir wollen hier ganz klassisch eine .xml-Datei erstellen, die wir später per SDK hochladen. Die Reihenfolge der Tags ist irrelevant, ich habe sie so angeordnet, dass mir das manuelle Durchsehen der Datei einfach fällt. Wie das mit PHP geht, dazu später mehr, fangen wir mit dem Aufbau an:

<?xml version="1.0" encoding="UTF-8"?>
<BulkDataExchangeRequests>
<Header>
<Version>669</Version>
<SiteID>77</SiteID>
</Header>

Die Versionsnummer steht für die Version von BulkDataExchange, dieses Tag wird auch erst am Ende der Datei geschlossen. Die SiteID 77 steht für Deutschland. Andere Länder können in der API Dokumentation  nachgeschlagen werden.

<AddFixedPriceItemRequest xmlns="urn:ebay:apis:eBLBaseComponents">
<ErrorLanguage>de_DE</ErrorLanguage>
<WarningLevel>Low</WarningLevel>
<Version>619</Version>

Für Produkte mit einem festen Preis wird hier der AddFixedPriceItemRequest verwendet. Damit wir erstmal eine Übersicht über die Fehler bekommen, können wir das WarningLevel auf Low stellen, sobald dort nichts mehr auftaucht, kann man das auch auch High stellen. Die API Version ist hier 619.

1.2. Item

Jetzt soll das Item definiert werden. Mit Item ist hier der übergeordnete/Eltern-/Parent-Artikel gemeint, eine Variation davon wird erst später definiert. Pro AddFixedPriceItemRequest gibt es nur ein Item, ein BulkDataExchangeRequest kann allerdings mehrere AddFixedPriceItemRequests enthalten. Für das Item gibt es eine umfangreiche Dokumentation, ich gehe hier die wichtigsten Tags durch:

<Country>DE</Country>
<Title>Supermarke Superjacke Unisex</Title>
<SubTitle>Diese Jacke ist einfach super!</SubTitle>
<InventoryTrackingMethod>SKU</InventoryTrackingMethod>
<SKU>2342</SKU>

Für Country werden (überwiegend) die zweistelligen Country Codes nach ISO 3166 benutzt. Der Title ist frei wählbar und darf bis zu 80 Zeichen lang sein. Am besten sollte hier schon bei der Ausgabe aus der Datenbank darauf geachtet werden und ggf. sinnvoll abgeschnitten werden. Der SubTitle ist ein kleiner grauer Untertitel, der in der eBay Suche unter dem eigentlichen Titel angezeigt wird. Achtung: er kostet pro Listing aber 1€ extra!

Es gibt mehrere Wege das Inventar bei eBay wiederzufinden, wir wählen hier SKU (Stock Keeping Unit, dt. Bestandseinheit) und geben sie auch gleich im Tag SKU mit an. Es ist natürlich auch möglich die Artikel über von eBay vergebene Item-ID zu verwalten, dafür muss InventoryTrackingMethod auf ItemID gesetzt werden, die Item-ID kommt dann mit der AddFixedPriceItemResponse, also der Antwort auf diesen Request. Ein weiteres Feld muss dann nicht ausgefüllt werden und das Tag SKU fällt weg.

<Description>Das ist eine wirklich super Jacke, sie hat die und die Eigenschaften.</Description>
<Currency>EUR</Currency>
<Location>Entenhausen</Location>
<DispatchTimeMax>1</DispatchTimeMax>
<ListingDuration>GTC</ListingDuration>
<ListingType>FixedPriceItem</ListingType>

In der Description soll der Artikel beschrieben werden, hier kann auch HTML verwendet werden, allerdings nur eingeschränkt. So funktionieren (bei eBay Deutschland) z.B. Links und Bilder, aber kein JavaScript oder iFrames. Die Currency ist der ISO 4217 Currency Code für die verwendete Währung. Dies ist auch die Währung für alle Variationen, da diese nicht in einer anderen Währung verkauft werden können. In der Location kann der Artikelstandort angebeben werden. Das ist natürlich bei mehreren Lagern problematisch, genau wie der nächste Tag.

Die DispatchTimeMax ist die maximale Anzahl an Tagen, bis der Artikel an den Versanddienstleister übergeben wird. Um den Stempel eBay-Garantie zu bekommen muss sie “1” betragen. Sie sollte aber auch generell möglichst gering gehalten werden, um mehr Verkäufe zu erzielen, da Kunden prinzipiell eher kaufen, wenn der Artikel schnell geliefert werden kann. Diese Zeit wird automatisch mit der Versandzeit (s. unten) verrechnet, um das ungefähre Lieferdatum automatisch zu berechnen. Allerdings gibt es hier ein Problem: diese Zeit kann nur Item-Ebene angegeben werden, eine unterschiedliche Zeit – je nach Variation – kann in der Realität jedoch vorkommen. So könnte die Jacke in Größe S und Farbe schwarz in einem anderen Lager liegen als dieselbe Jacke in Größe M und Farbe rot. Oder sie könnte nachbestellt sein, also auf dem Weg ins Lager sein (wo die DispatchTime dann 1 Tag wäre), man möchte aber natürlich die Anzahl dieser Variation nicht auf 0 setzen, da sie ja verfügbar ist – nur eben nicht ganz so schnell. Von eBay gibt es hier leider wenig Hilfe bzw. Hinweise, wie mit der Situation umzugehen sei. Ein Hinweis auf eine ggf. längere Lieferzeit sollte also in so einem Fall zumindest in der Produktbeschreibung vorhanden sein, falls ein Kunde sich bei eBay beschwert. Aber: die Beschreibung kann nicht mehr geändert werden, sobald ein Kunde irgendeine Variation eines Artikels gekauft hat, man kann also nicht dynamisch nach Verfügbarkeit den Hinweis entfernen oder hinzufügen. Als work-around wäre z.B. auch denkbar eine Lieferzeit anhand der Item-Eigenschaften (z.B. Farbe und Größe) auf der eigenen Webseite zur Verfügung zu stellen, ggf. mit dynamischen Link in der Produktbeschreibung. So kommt es in der Regel zu weniger Support Tickets und zu glücklicheren Kunden :)

Die ListingDuration bei den meisten Artikeln dürfte auf GTC (Good ‘Till Cancelled, dt. gültig bis Widerruf), möglich sind hier auch Werte wie Days_14 oder Days_30, welche man z.B. für Aktionen und Abverkäufe nutzen kann. Die Valid Values variieren jedoch je nach Kategorie (s. unten) und können über den API Call getCategoryDetails abgefragt werden. Außerdem ist der Wert GTC in der Sandbox nicht möglich, dort sollte man Days_30 versuchen.

Warum man den ListingType hier noch einmal angeben muss erschließt sich mit nicht ganz, da diese Information ja im Container AddFixedPriceItem steht.

<ItemSpecifics>
<NameValueList>
<Name>Material</Name>
<Value>100% Baumwolle</Value>
</NameValueList>
<NameValueList>
<Name>Herstellernummer</Name>
<Value>1337</Value>
</NameValueList>
</ItemSpecifics>

Die ItemSpecifis sind Werte, die direkt über dem Artikel angezeigt werden. Dazu zählen der Artikelzustand (s. unten) und die hier angegebenen Felder. Mit dem API Call getCategorySpecifics kann man für eine Kategorie empfohlene Werte bekommen (alternativ keine angeben und dann im Backend den Artikel anklicken, dort wird man auch darauf hingewiesen), im Prinzip können die Felder jedoch mit freien Strings befüllt werden. Die vorgeschlagenen Werte dienen dazu, dass eBay Nutzer auf der Website die Suche sinnvoll eingrenzen können, z.B. nach Herstellernummer oder Marke.

<CategoryMappingAllowed>true</CategoryMappingAllowed>
<CategoryBasedAttributesPrefill>true</CategoryBasedAttributesPrefill>
<PrimaryCategory>
<CategoryID>109149</CategoryID>
</PrimaryCategory>
<SecondaryCategory>
<CategoryID>63862</CategoryID>
</SecondaryCategory>

PrimaryCategory ist die erste eBay Kategorie. Diese können z.B. aus dem Java Applet im CSV Manager oder aus der Liste entnommen werden. Am besten ist es, die Artikel so weit wie möglich “nach unten” zu hängen. Außerdem ist diese Kategorie dafür verantwortlich, wie viel eBay Gebühren gezahlt werden. So sind Artikel im Bereich Sport z.B. günstiger als im Kleidungsbereich. Wenn man also Sportkleidung verkauft, wäre es günstiger, als erste Kategorie eine Unterkategorie des Sportbereichs anzugeben. In der zweiten Kategorie SecondaryCategory kann dann immer noch Kleidung angegeben werden. Für manche Produkte spielt dies aber auch keine Rolle, da viele Kategorien gleich behandelt werden.

Das Tag CategoryMappingAllowed erlaubt es eBay, bei einer Änderung der Kategoriestruktur, die alten Kategorien in die neuen Kategorien umzuwandeln (= zu “mappen”). So stellt man einen reibungslosen Ablauf bei automatisiertem Listing sicher. Andernfalls (also auf false) würde das Produkt nicht gelistet werden und es würde ein Fehler in der AddFixedPriceItemResponse auftauchen.

Mit CategoryBasedAttributesPrefill wird eBay erlaubt, aufgrund der Kategorie(n) (s. oben) schon einige Item spezifische Attribute (Item Specifics, s. oben) auszufüllen. Denkbar wäre z.B. dass alle Items in  der Kategorie Damenjacken in den Item Specifics schon den Gender Damen erhalten.

<PaymentMethods>CCAccepted</PaymentMethods>
<PaymentMethods>Moneybookers</PaymentMethods>
<PaymentMethods>PayPal</PaymentMethods>
<PayPalEmailAddress>paypal@firma.de</PayPalEmailAddress>

Die Bezahloptionen können einfach mit mehreren PaymentMethods Tags untereinander gereiht werden. Wichtig ist hierbei, dass die PayPalEmailAddress als Letztes kommt. Valid Values können in der API Dokumentation nachgeschaut werden, allerdings sind nicht alle Werte immer zulässig. Die E-Mail Adresse muss immer die sein, mit der sich zu erst bei PayPal angemeldet wurde, sonst gibt es eine Warning.

<ConditionID>1000</ConditionID>
<OutOfStockControl>true</OutOfStockControl>

Die ConditionID zeigt in den Item Specifics an, ob der Artikel neu oder gebraucht ist. Die Werte kann man über ein Look-Up Table nachschlagen. Wenn die Verfügbarkeit (Quantity) eines Artikels auf 0 sinkt, wird normalerweise das Listing beendet. Seit Version 823 (vom 5.8.2013) kann man mit dem Tag OutOfStockControl aber das Listing aktiv aber unterdrückt lassen, falls z.B. neue Ware bereits auf dem Weg ist. Dies erspart ein erneutes Listen und damit (außer bei Premium Shops) natürlich auch die Listingkosten.

<ReturnPolicy>
<RefundOption>EUSeller_ReturnRights</RefundOption>
<ReturnsAcceptedOption>ReturnsAccepted</ReturnsAcceptedOption>
<ReturnsWithinOption>Months_1</ReturnsWithinOption>
<Description>Zurückgeben ist kein Problem. Hier steht wie es geht.</Description>
<ShippingCostPaidByOption>Seller</ShippingCostPaidByOption>
</ReturnPolicy>

Die ReturnPolicy ist wichtig für die eBay Garantie. Die Valid Values für RefundOption können auch hier der API Dokumentation entnommen werden oder über den API Call getEbayDetails angefragt werden (daher stammt EUSeller_ReturnRights). Außerdem können diese Details auch im Backend hinterlegt werden. Um die eBay Garantie zu erreichen, sollte ReturnsWithinOption auf einen Monat (Months_1) gesetzt werden. Achtung: Das ist nicht gleichbedeutend mit Days_30. In der Description sollte ein kurzer Text (max. 5000 Zeichen) den Rückgabsprozess erklären. Natürlich könnte man ReturnsAcceptedOption auch auf ReturnsNotAccepted (den einzig anderen Valid Value aus der API Doku) setzen, das wirkt sich aber sicherlich schlecht auf die Verkaufszahlen aus. Schließlich gibt es mit dem ShippingOptionPaidByOption Tag noch ein paar Valid Values um anzugeben, unter welchen Umständen und wer die Rücksendekosten zu tragen hat.

<ShippingDetails>
<ShippingType>Flat</ShippingType>
<ShippingServiceOptions>
<ShippingServicePriority>1</ShippingServicePriority>
<ShippingService>DE_DHLPaket</ShippingService>
<ShippingServiceCost currency="EUR">0.0</ShippingServiceCost>
</ShippingServiceOptions>
</ShippingDetails>

Die ShippingDetails geben die Versand Optionen an. Auch hier gibt es Voreinstellungen im Verkäufer Backend. In diesem Beispiel bietet man kostenlosen Versand an, eine Vorraussetzung für die eBay Garantie. Die Valid Values für ShippingService stehen in einer ziemlich langen Liste in der API Dokumentation. Aufgrund dieser Angabe und der DispatchTimeMax (s. oben) wird der vorläufige Lieferzeitpunkt automatisch errechnet und im Listing angezeigt. Man kann mehrere ShippingServiceOptions definieren und diesen Optionen jeweils eine Priorität in der Auflistung geben, die ShippingServicePriority. Je höher die Zahl desto weiter hinten steht die Option.

<PictureDetails>
<PictureURL>http://eigener-server.de/images/main-bild-2342.jpg</PictureURL>
</PictureDetails>

Als letztes kann noch ein Bild spezifiziert werden. Dieses Bild kann keiner Variante zugeordnet werden (“main”-Bild), daher ist es häufig ein Bild, was alle Variationen in einem Bild zeigt. Kann kann Bilder theoretisch auch bei Item und Variante zeigen, allerdings taucht es dann in der Item Ansicht doppelt auf. Ich gehe hier davon aus, dass die Bilder selbst gehostet werden. Man kann hier für auch den eBay Picture Service (EPS) nutzen, dazu mehr in der eBay Hilfe.

Die Verfügbarkeit (Tag Quantity) und den Preis (Tag StartPrice) müssen auf Item Ebene nicht angegeben werden, wenn sie in den Variationen definiert werden. Gibt man den Wert trotzdem an, wird er ignoriert.

1.3. Variationen

Die Variationen werden alle in dem überordneten Tag Variations (mit s!) angegeben. Wir eröffnen also die Variationenlistung mit diesem Tag

<Variations>

Als erstes empfehle ich erst einmal die Variation Specifics (das Pendant zu Item Specifics) im VariationSpecificsSet aufzuzählen, also alle möglichen Werte für die verschiedenen Dimensionen in denen es dieses Produkt gibt. Bei Kleidung könnten dies z.B. die folgenden Werte sein:

<VariationSpecificsSet>
<NameValueList>
<Name>Farbe</Name>
<Value>schwarz</Value>
<Value>rot</Value>
</NameValueList>
<NameValueList>
<Name>Größe</Name>
<Value>S</Value>
<Value>M</Value>
<Value>L</Value>
</NameValueList>
</VariationSpecificsSet>

Jetzt erst folgt die erste Variation (ohne s). Hier dürfen in den VariationSpecifics nur Werte angegeben werden, die auch oben spezifiziert wurden. Nicht alle Händler sind außerdem dazu berechtigt den OriginalRetailPrice (dt. unverbindliche Preisempfehlung UVP) anzugeben, am besten kontaktiert man hier den eBay Support. Die SKU ist hier eine spezielle SKU für ein Produkt in Abhängigkeit von den Dimensionen (z.B. Farbe und Größe). Sollte eine solche SKU nicht existieren, wäre eine Idee, sie aus Parent-SKU und Kürzeln für die Dimensionen zusammenzubauen (hier: 2342 => ParentID, rS => Farbe: rot, Größe: S). Die Quantity ist die verfügbare Anzahl in den angegebenen Dimensionen und obwohl der Tag StartPrice heisst, ist hier auch bei FixedPriceItems der Verkaufspreis gemeint.

<Variation>
<SKU>2342-rS</SKU>
<Quantity>123</Quantity>
<StartPrice>99.00</StartPrice>
<DiscountPriceInfo>
<OriginalRetailPrice>129.00</OriginalRetailPrice>
</DiscountPriceInfo>
<VariationSpecifics>
<NameValueList>
<Name>Farbe</Name>
<Value>rot</Value>
</NameValueList>
<NameValueList>
<Name>Größe</Name>
<Value>S</Value>
</NameValueList>
</VariationSpecifics>
</Variation>

Die Bilder für die verschiedenen Variationen werden widererwartend nicht im Variation Tag angegeben, sondern anhand der vorher definierten Dimensionen im Pictures Tag gemappt. Dabei reicht eine Dimension aus. Bei Kleidung z.B. wäre hier die Farbe relevant, die Größe spielt keine Rolle.

<Pictures>
<VariationSpecificName>Farbe</VariationSpecificName>
<VariationSpecificPictureSet>
<VariationSpecificValue>rot</VariationSpecificValue>
<PictureURL>http://eigener-server/images/variationen/bild-2342-rot.jpg</PictureURL>
</VariationSpecificPictureSet>
</Pictures>

Das war es auch schon fast. Jetzt müssen nur noch alle offenen Tags geschlossen werden.

</Variations>
</Item>
</AddFixedPriceItemRequest>

Jetzt können noch weitere AddFixedPriceItemRequest angehängt werden, bis die Datei 20.000 Zeilen lang ist. Dann muss auch der BulkDataExchangeRequest geschlossen werden.

</BulkDataExchangeRequests>

Wie man diese Datei in PHP erstellt, wird im zweiten Teil erklärt.

flattr this!