<?php
defined ('_JEXEC') or die();
/**
 * Klarna API
 *
 * PHP Version 5.3
 *
 * @category  Payment
 * @package   KlarnaAPI
 * @author    MS Dev <ms.modules@klarna.com>
 * @copyright 2012 Klarna AB (http://klarna.com)
 * @license   http://opensource.org/licenses/BSD-2-Clause BSD-2
 * @link      http://integration.klarna.com/
 */

/**
 * This API provides a way to integrate with Klarna's services over the
 * XMLRPC protocol.
 *
 * All strings inputted need to be encoded with ISO-8859-1.<br>
 * In addition you need to decode HTML entities, if they exist.<br>
 *
 * For more information see our
 * {@link http://integration.klarna.com/en/api/step-by-step step by step} guide.
 *
 * Dependencies:
 *
 *  xmlrpc-3.0.0.beta/lib/xmlrpc.inc
 *      from {@link http://phpxmlrpc.sourceforge.net/}
 *
 * xmlrpc-3.0.0.beta/lib/xmlrpc_wrappers.inc
 *      from {@link http://phpxmlrpc.sourceforge.net/}
 *
 * @category  Payment
 * @package   KlarnaAPI
 * @author    MS Dev <ms.modules@klarna.com>
 * @copyright 2012 Klarna AB (http://klarna.com)
 * @license   http://opensource.org/licenses/BSD-2-Clause BSD-2
 * @link      http://integration.klarna.com/
 */
class Klarna
{
    /**
     * Klarna PHP API version identifier.
     *
     * @var string
     */
    protected $VERSION = 'php:api:2.4.2';

    /**
     * Klarna protocol identifier.
     *
     * @var string
     */
    protected $PROTO = '4.1';

    /**
     * Flag to indicate use of the report server Candice.
     *
     * @var bool
     */
    private static $_candice = true;

    /**
     * URL/Address to the Candice server.
     * Port used is 80.
     *
     * @var string
     */
    private static $_c_addr = "clientstat.klarna.com";

    /**
     * Constants used with LIVE mode for the communications with Klarna.
     *
     * @var int
     */
    const LIVE = 0;

    /**
     * URL/Address to the live Klarna Online server.
     * Port used is 443 for SSL and 80 without.
     *
     * @var string
     */
    private static $_live_addr = 'payment.klarna.com';

    /**
     * Constants used with BETA mode for the communications with Klarna.
     *
     * @var int
     */
    const BETA = 1;

    /**
     * URL/Address to the beta test Klarna Online server.
     * Port used is 443 for SSL and 80 without.
     *
     * @var string
     */
    private static $_beta_addr = 'payment.testdrive.klarna.com';

    /**
     * Indicates whether the communications is over SSL or not.
     *
     * @var bool
     */
    protected $ssl = false;

    /**
     * An object of xmlrpc_client, used to communicate with Klarna.
     *
     * @link http://phpxmlrpc.sourceforge.net/
     *
     * @var xmlrpc_client
     */
    protected $xmlrpc;

    /**
     * Which server the Klarna API is using, LIVE or BETA (TESTING).
     *
     * @see Klarna::LIVE
     * @see Klarna::BETA
     *
     * @var int
     */
    protected $mode;

    /**
     * Associative array holding url information.
     *
     * @var array
     */
    private $_url;

    /**
     * The estore's identifier received from Klarna.
     *
     * @var int
     */
    private $_eid;

    /**
     * The estore's shared secret received from Klarna.
     *
     * <b>Note</b>:<br>
     * DO NOT SHARE THIS WITH ANYONE!
     *
     * @var string
     */
    private $_secret;

    /**
     * KlarnaCountry constant.
     *
     * @see KlarnaCountry
     *
     * @var int
     */
    private $_country;

    /**
     * KlarnaCurrency constant.
     *
     * @see KlarnaCurrency
     *
     * @var int
     */
    private $_currency;

    /**
     * KlarnaLanguage constant.
     *
     * @see KlarnaLanguage
     *
     * @var int
     */
    private $_language;

    /**
     * An array of articles for the current order.
     *
     * @var array
     */
    protected $goodsList;

    /**
     * An array of article numbers and quantity.
     *
     * @var array
     */
    protected $artNos;

    /**
     * An KlarnaAddr object containing the billing address.
     *
     * @var KlarnaAddr
     */
    protected $billing;

    /**
     * An KlarnaAddr object containing the shipping address.
     *
     * @var KlarnaAddr
     */
    protected $shipping;

    /**
     * Estore's user(name) or identifier.
     * Only used in {@link Klarna::addTransaction()}.
     *
     * @var string
     */
    protected $estoreUser = "";

    /**
     * External order numbers from other systems.
     *
     * @var string
     */
    protected $orderid = array("", "");

    /**
     * Reference (person) parameter.
     *
     * @var string
     */
    protected $reference = "";

    /**
     * Reference code parameter.
     *
     * @var string
     */
    protected $reference_code = "";

    /**
     * An array of named extra info.
     *
     * @var array
     */
    protected $extraInfo = array();

    /**
     * An array of named bank info.
     *
     * @var array
     */
    protected $bankInfo = array();

    /**
     * An array of named income expense info.
     *
     * @var array
     */
    protected $incomeInfo = array();

    /**
     * An array of named shipment info.
     *
     * @var array
     */
    protected $shipInfo = array();

    /**
     * An array of named travel info.
     *
     * @ignore Do not show this in PHPDoc.
     * @var array
     */
    protected $travelInfo = array();

    /**
     * An array of named activate info
     *
     * @ignore
     * @var array
     */
    protected $activateInfo = array();

    /**
     * An array of named session id's.<br>
     * E.g. "dev_id_1" => ...<br>
     *
     * @var array
     */
    protected $sid = array();

    /**
     * A comment sent in the XMLRPC communications.
     * This is resetted using clear().
     *
     * @var string
     */
    protected $comment = "";

    /**
     * An array with all the checkoutHTML objects.
     *
     * @var array
     */
    protected $coObjects = array();

    /**
     * Flag to indicate if the API should output verbose
     * debugging information.
     *
     * @var bool
     */
    public static $debug = false;

    /**
     * Turns on the internal XMLRPC debugging.
     *
     * @var bool
     */
    public static $xmlrpcDebug = false;

    /**
     * If this is set to true, XMLRPC invocation is disabled.
     *
     * @var bool
     */
    public static $disableXMLRPC = false;

    /**
     * If the estore is using a proxy which populates the clients IP to
     * x_forwarded_for
     * then and only then should this be set to true.
     *
     * <b>Note</b>:<br>
     * USE WITH CARE!
     *
     * @var bool
     */
    public static $x_forwarded_for = false;

    /**
     * Array of HTML entities, used to create numeric htmlentities.
     *
     * @ignore Do not show this in PHPDoc.
     * @var array
     */
    protected static $htmlentities = false;

    /**
     * Populated with possible proxy information.
     * A comma separated list of IP addresses.
     *
     * @var string
     */
    private $_x_fwd;

    /**
     * The storage class for PClasses.
     *
     * Use 'xml' for xmlstorage.class.php.<br>
     * Use 'mysql' for mysqlstorage.class.php.<br>
     * Use 'json' for jsonstorage.class.php.<br>
     *
     * @var string
     */
    protected $pcStorage;

    /**
     * The storage URI for PClasses.
     *
     * Use the absolute or relative URI to a file if
     * {@link Klarna::$pcStorage} is set as 'xml' or 'json'.<br>
     * Use a HTTP-auth similar URL if {@link Klarna::$pcStorage} is set
     * as 'mysql', <br>
     * e.g. user:passwd@addr:port/dbName.dbTable.<br>
     * Or an associative array (recommended) {@see MySQLStorage}
     *
     * @var mixed
     */
    protected $pcURI;

    /**
     * PCStorage instance.
     *
     * @ignore Do not show this in PHPDoc.
     * @var PCStorage
     */
    protected $pclasses;

    /**
     * ArrayAccess instance.
     *
     * @ignore Do not show this in PHPDoc.
     * @var ArrayAccess
     */
    protected $config;

    /**
     * Empty constructor, because sometimes it's needed.
     */
    public function __construct()
    {
    }

    /**
     * Checks if the config has fields described in argument.<br>
     * Missing field(s) is in the exception message.
     *
     * To check that the config has eid and secret:<br>
     * <code>
     * try {
     *     $this->hasFields('eid', 'secret');
     * }
     * catch(Exception $e) {
     *     echo "Missing fields: " . $e->getMessage();
     * }
     * </code>
     *
     * @throws Exception
     * @return void
     */
    protected function hasFields(/*variable arguments*/)
    {
        $missingFields = array();
        $args = func_get_args();
        foreach ($args as $field) {
            if (!isset($this->config[$field])) {
                $missingFields[] = $field;
            }
        }
        if (count($missingFields) > 0) {
            throw new Klarna_ConfigFieldMissingException(
                implode(', ', $missingFields)
            );
        }
    }

    /**
     * Initializes the Klarna object accordingly to the set config object.
     *
     * @throws KlarnaException
     * @return void
     */
    protected function init()
    {
        $this->hasFields('eid', 'secret', 'mode', 'pcStorage', 'pcURI');

        if (!is_int($this->config['eid'])) {
            $this->config['eid'] = intval($this->config['eid']);
        }

        if ($this->config['eid'] <= 0) {
            throw new Klarna_ConfigFieldMissingException('eid');
        }

        if (!is_string($this->config['secret'])) {
            $this->config['secret'] = strval($this->config['secret']);
        }

        if (strlen($this->config['secret']) == 0) {
            throw new Klarna_ConfigFieldMissingException('secret');
        }

        //Set the shop id and secret.
        $this->_eid = $this->config['eid'];
        $this->_secret = $this->config['secret'];

        //Set the country specific attributes.
        try {
            $this->hasFields('country', 'language', 'currency');

            //If hasFields doesn't throw exception we can set them all.
            $this->setCountry($this->config['country']);
            $this->setLanguage($this->config['language']);
            $this->setCurrency($this->config['currency']);
        } catch(Exception $e) {
            //fields missing for country, language or currency
            $this->_country = $this->_language = $this->_currency = null;
        }

        //Set addr and port according to mode.
        $this->mode = (int)$this->config['mode'];

        $this->_url = array();

        // If a custom url has been added to the config, use that as xmlrpc
        // recipient.
        if (isset($this->config['url'])) {
            $this->_url = parse_url($this->config['url']);
            if ($this->_url === false) {
                $message = "Configuration value 'url' could not be parsed. " .
                    "(Was: '{$this->config['url']}')";
                Klarna::printDebug(__METHOD__, $message);
                throw new InvalidArgumentException($message);
            }
        } else {

            $this->_url['scheme'] = 'https';

            if ($this->mode === self::LIVE) {
                $this->_url['host'] = self::$_live_addr;
            } else {
                $this->_url['host'] = self::$_beta_addr;
            }

            if (isset($this->config['ssl'])
                && (bool)$this->config['ssl'] === false
            ) {
                $this->_url['scheme'] = 'http';
            }
        }

        // If no port has been specified, deduce from url scheme
        if (!array_key_exists('port', $this->_url)) {
            if ($this->_url['scheme'] === 'https') {
                $this->_url['port'] = 443;
            } else {
                $this->_url['port'] = 80;
            }
        }

        try {
            $this->hasFields('candice');
            self::$_candice = (bool)$this->config['candice'];
        } catch(Exception $e) {
            //No 'candice' field ignore it...
        }

        try {
            $this->hasFields('xmlrpcDebug');
            Klarna::$xmlrpcDebug = $this->config['xmlrpcDebug'];
        } catch(Exception $e) {
            //No 'xmlrpcDebug' field ignore it...
        }

        try {
            $this->hasFields('debug');
            Klarna::$debug = $this->config['debug'];
        } catch(Exception $e) {
            //No 'debug' field ignore it...
        }

        $this->pcStorage = $this->config['pcStorage'];
        $this->pcURI = $this->config['pcURI'];

        // Default path to '/' if not set.
        if (!array_key_exists('path', $this->_url)) {
            $this->_url['path'] = '/';
        }

        $this->xmlrpc = new xmlrpc_client(
            $this->_url['path'],
            $this->_url['host'],
            $this->_url['port'],
            $this->_url['scheme']
        );

        $this->xmlrpc->request_charset_encoding = 'ISO-8859-1';
    }

    /**
     * Method of ease for setting common config fields.
     *
     * The storage module for PClasses:<br>
     * Use 'xml' for xmlstorage.class.php.<br>
     * Use 'mysql' for mysqlstorage.class.php.<br>
     * Use 'json' for jsonstorage.class.php.<br>
     *
     * The storage URI for PClasses:<br>
     * Use the absolute or relative URI to a file if {@link Klarna::$pcStorage}
     * is set as 'xml' or 'json'.<br>
     * Use a HTTP-auth similar URL if {@link Klarna::$pcStorage} is set as
     * mysql', e.g. user:passwd@addr:port/dbName.dbTable.
     * Or an associative array (recommended) {@see MySQLStorage}
     *
     * <b>Note</b>:<br>
     * This disables the config file storage.<br>
     *
     * @param int    $eid       Merchant ID/EID
     * @param string $secret    Secret key/Shared key
     * @param int    $country   {@link KlarnaCountry}
     * @param int    $language  {@link KlarnaLanguage}
     * @param int    $currency  {@link KlarnaCurrency}
     * @param int    $mode      {@link Klarna::LIVE} or {@link Klarna::BETA}
     * @param string $pcStorage PClass storage module.
     * @param string $pcURI     PClass URI.
     * @param bool   $ssl       Whether HTTPS (HTTP over SSL) or HTTP is used.
     * @param bool   $candice   Error reporting to Klarna.
     *
     * @see Klarna::setConfig()
     * @see KlarnaConfig
     *
     * @throws KlarnaException
     * @return void
     */
    public function config(
        $eid, $secret, $country, $language, $currency,
        $mode = Klarna::LIVE, $pcStorage = 'json', $pcURI = 'pclasses.json',
        $ssl = true, $candice = true
    ) {
        try {
            KlarnaConfig::$store = false;
            $this->config = new KlarnaConfig(null);

            $this->config['eid'] = $eid;
            $this->config['secret'] = $secret;
            $this->config['country']  = $country;
            $this->config['language'] = $language;
            $this->config['currency'] = $currency;
            $this->config['mode'] = $mode;
            $this->config['ssl'] = $ssl;
            $this->config['candice'] = $candice;
            $this->config['pcStorage'] = $pcStorage;
            $this->config['pcURI'] = $pcURI;

            $this->init();
        } catch(Exception $e) {
            $this->config = null;
            throw new KlarnaException(
                $e->getMessage(),
                $e->getCode()
            );
        }
    }

    /**
     * Sets and initializes this Klarna object using the supplied config object.
     *
     * @param KlarnaConfig &$config Config object.
     *
     * @see KlarnaConfig
     * @throws  KlarnaException
     * @return  void
     */
    public function setConfig(&$config)
    {
        $this->_checkConfig($config);

        $this->config = $config;
        $this->init();
    }

    /**
     * Get the complete locale (country, language, currency) to use for the
     * values passed, or the configured value if passing null.
     *
     * @param mixed $country  country  constant or code
     * @param mixed $language language constant or code
     * @param mixed $currency currency constant or code
     *
     * @throws KlarnaException
     * @return array
     */
    public function getLocale(
        $country = null, $language = null, $currency = null
    ) {
        $locale = array(
            'country' => null,
            'language' => null,
            'currency' => null
        );

        if ($country === null) {
            // Use the configured country / language / currency
            $locale['country'] = $this->_country;
            if ($this->_language !== null) {
                $locale['language'] = $this->_language;
            }

            if ($this->_currency !== null) {
                $locale['currency'] = $this->_currency;
            }
        } else {
            // Use the given country / language / currency
            if (!is_numeric($country)) {
                $country = KlarnaCountry::fromCode($country);
            }
            $locale['country'] = intval($country);

            if ($language !== null) {
                if (!is_numeric($language)) {
                    $language = KlarnaLanguage::fromCode($language);
                }
                $locale['language'] = intval($language);
            }

            if ($currency !== null) {
                if (!is_numeric($currency)) {
                    $currency = KlarnaCurrency::fromCode($currency);
                }
                $locale['currency'] = intval($currency);
            }
        }

        // Complete partial structure with defaults
        if ($locale['currency'] === null) {
            $locale['currency'] = $this->getCurrencyForCountry(
                $locale['country']
            );
        }

        if ($locale['language'] === null) {
            $locale['language'] = $this->getLanguageForCountry(
                $locale['country']
            );
        }

        $this->_checkCountry($locale['country']);
        $this->_checkCurrency($locale['currency']);
        $this->_checkLanguage($locale['language']);

        return $locale;
    }

    /**
     * Sets the country used.
     *
     * <b>Note</b>:<br>
     * If you input 'dk', 'fi', 'de', 'nl', 'no' or 'se', <br>
     * then currency and language will be set to mirror that country.<br>
     *
     * @param string|int $country {@link KlarnaCountry}
     *
     * @see KlarnaCountry
     *
     * @throws KlarnaException
     * @return void
     */
    public function setCountry($country)
    {
        if (!is_numeric($country)
            && (strlen($country) == 2 || strlen($country) == 3)
        ) {
            $country = KlarnaCountry::fromCode($country);
        }
        $this->_checkCountry($country);
        $this->_country = $country;
    }

    /**
     * Returns the country code for the set country constant.
     *
     * @param int $country {@link KlarnaCountry Country} constant.
     *
     * @return string  Two letter code, e.g. "se", "no", etc.
     */
    public function getCountryCode($country = null)
    {
        if ($country === null) {
            $country = $this->_country;
        }

        $code = KlarnaCountry::getCode($country);
        return (string) $code;
    }

    /**
     * Returns the {@link KlarnaCountry country} constant from the country code.
     *
     * @param string $code Two letter code, e.g. "se", "no", etc.
     *
     * @throws KlarnaException
     * @return int {@link KlarnaCountry Country} constant.
     */
    public static function getCountryForCode($code)
    {
        $country = KlarnaCountry::fromCode($code);
        if ($country === null) {
            throw new Klarna_UnknownCountryException($code);
        }
        return $country;
    }

    /**
     * Returns the country constant.
     *
     * @return int  {@link KlarnaCountry}
     */
    public function getCountry()
    {
        return $this->_country;
    }

    /**
     * Sets the language used.
     *
     * <b>Note</b>:<br>
     * You can use the two letter language code instead of the constant.<br>
     * E.g. 'da' instead of using {@link KlarnaLanguage::DA}.<br>
     *
     * @param string|int $language {@link KlarnaLanguage}
     *
     * @see KlarnaLanguage
     *
     * @throws KlarnaException
     * @return void
     */
    public function setLanguage($language)
    {
        if (!is_numeric($language) && strlen($language) == 2) {
            $this->setLanguage(self::getLanguageForCode($language));
        } else {
            $this->_checkLanguage($language);
            $this->_language = $language;
        }
    }

    /**
     * Returns the language code for the set language constant.
     *
     * @param int $language {@link KlarnaLanguage Language} constant.
     *
     * @return string Two letter code, e.g. "da", "de", etc.
     */
    public function getLanguageCode($language = null)
    {
        if ($language === null) {
            $language = $this->_language;
        }
        $code = KlarnaLanguage::getCode($language);

        return (string) $code;
    }

    /**
     * Returns the {@link KlarnaLanguage language} constant from the language code.
     *
     * @param string $code Two letter code, e.g. "da", "de", etc.
     *
     * @throws KlarnaException
     * @return int  {@link KlarnaLanguage Language} constant.
     */
    public static function getLanguageForCode($code)
    {
        $language = KlarnaLanguage::fromCode($code);

        if ($language === null) {
            throw new Klarna_UnknownLanguageException($code);
        }
        return $language;
    }

    /**
     * Returns the language constant.
     *
     * @return int  {@link KlarnaLanguage}
     */
    public function getLanguage()
    {
        return $this->_language;
    }

    /**
     * Sets the currency used.
     *
     * <b>Note</b>:<br>
     * You can use the three letter shortening of the currency.<br>
     * E.g. "dkk", "eur", "nok" or "sek" instead of the constant.<br>
     *
     * @param string|int $currency {@link KlarnaCurrency}
     *
     * @see KlarnaCurrency
     *
     * @throws KlarnaException
     * @return void
     */
    public function setCurrency($currency)
    {
        if (!is_numeric($currency) && strlen($currency) == 3) {
            $this->setCurrency(self::getCurrencyForCode($currency));
        } else {
            $this->_checkCurrency($currency);
            $this->_currency = $currency;
        }
    }

    /**
     * Returns the {@link KlarnaCurrency currency} constant from the currency
     * code.
     *
     * @param string $code Two letter code, e.g. "dkk", "eur", etc.
     *
     * @throws KlarnaException
     * @return int  {@link KlarnaCurrency Currency} constant.
     */
    public static function getCurrencyForCode($code)
    {
        $currency = KlarnaCurrency::fromCode($code);
        if ($currency === null) {
            throw new Klarna_UnknownCurrencyException($code);
        }
        return $currency;
    }

    /**
     * Returns the the currency code for the set currency constant.
     *
     * @param int $currency {@link KlarnaCurrency Currency} constant.
     *
     * @return string  Three letter currency code.
     */
    public function getCurrencyCode($currency = null)
    {
        if ($currency === null) {
            $currency = $this->_currency;
        }

        $code = KlarnaCurrency::getCode($currency);
        return (string) $code;
    }

    /**
     * Returns the set currency constant.
     *
     * @return int  {@link KlarnaCurrency}
     */
    public function getCurrency()
    {
        return $this->_currency;
    }

    /**
     * Returns the {@link KlarnaLanguage language} constant for the specified
     * or set country.
     *
     * @param int $country {@link KlarnaCountry Country} constant.
     *
     * @deprecated Do not use.
     *
     * @return int|false if no match otherwise KlarnaLanguage constant.
     */
    public function getLanguageForCountry($country = null)
    {
        if ($country === null) {
            $country = $this->_country;
        }
        // Since getLanguage defaults to EN, check so we actually have a match
        $language = KlarnaCountry::getLanguage($country);
        if (KlarnaCountry::checkLanguage($country, $language)) {
            return $language;
        }
        return false;
    }

    /**
     * Returns the {@link KlarnaCurrency currency} constant for the specified
     * or set country.
     *
     * @param int $country {@link KlarnaCountry country} constant.
     *
     * @deprecated Do not use.
     *
     * @return int|false {@link KlarnaCurrency currency} constant.
     */
    public function getCurrencyForCountry($country = null)
    {
        if ($country === null) {
            $country = $this->_country;
        }
        return KlarnaCountry::getCurrency($country);
    }

    /**
     * Sets the session id's for various device identification,
     * behaviour identification software.
     *
     * <b>Available named session id's</b>:<br>
     * string - dev_id_1<br>
     * string - dev_id_2<br>
     * string - dev_id_3<br>
     * string - beh_id_1<br>
     * string - beh_id_2<br>
     * string - beh_id_3<br>
     *
     * @param string $name Session ID identifier, e.g. 'dev_id_1'.
     * @param string $sid  Session ID.
     *
     * @throws KlarnaException
     * @return void
     */
    public function setSessionID($name, $sid)
    {
        $this->_checkArgument($name, "name");
        $this->_checkArgument($sid, "sid");

        $this->sid[$name] = $sid;
    }

    /**
     * Sets the shipment information for the upcoming transaction.<br>
     *
     * Using this method is optional.
     *
     * <b>Available named values are</b>:<br>
     * int    - delay_adjust<br>
     * string - shipping_company<br>
     * string - shipping_product<br>
     * string - tracking_no<br>
     * array  - warehouse_addr<br>
     *
     * "warehouse_addr" is sent using {@link KlarnaAddr::toArray()}.
     *
     * Make sure you send in the values as the right data type.<br>
     * Use strval, intval or similar methods to ensure the right type is sent.
     *
     * @param string $name  key
     * @param mixed  $value value
     *
     * @throws KlarnaException
     * @return void
     */
    public function setShipmentInfo($name, $value)
    {
        $this->_checkArgument($name, "name");

        $this->shipInfo[$name] = $value;
    }

    /**
     * Sets the Activation information for the upcoming transaction.<br>
     *
     * Using this method is optional.
     *
     * <b>Available named values are</b>:<br>
     * int    - flags<br>
     * int    - bclass<br>
     * string - orderid1<br>
     * string - orderid2<br>
     * string - ocr<br>
     * string - reference<br>
     * string - reference_code<br>
     * string - cust_no<br>
     *
     * Make sure you send in the values as the right data type.<br>
     * Use strval, intval or similar methods to ensure the right type is sent.
     *
     * @param string $name  key
     * @param mixed  $value value
     *
     * @see setShipmentInfo
     *
     * @return void
     */
    public function setActivateInfo($name, $value)
    {
        $this->activateInfo[$name] = $value;
    }

    /**
     * Sets the extra information for the upcoming transaction.<br>
     *
     * Using this method is optional.
     *
     * <b>Available named values are</b>:<br>
     * string - cust_no<br>
     * string - estore_user<br>
     * string - maiden_name<br>
     * string - place_of_birth<br>
     * string - password<br>
     * string - new_password<br>
     * string - captcha<br>
     * int    - poa_group<br>
     * string - poa_pno<br>
     * string - ready_date<br>
     * string - rand_string<br>
     * int    - bclass<br>
     * string - pin<br>
     *
     * Make sure you send in the values as the right data type.<br>
     * Use strval, intval or similar methods to ensure the right type is sent.
     *
     * @param string $name  key
     * @param mixed  $value value
     *
     * @throws KlarnaException
     * @return void
     */
    public function setExtraInfo($name, $value)
    {
        $this->_checkArgument($name, "name");

        $this->extraInfo[$name] = $value;
    }

    /**
     * Sets the income expense information for the upcoming transaction.<br>
     *
     * Using this method is optional.
     *
     * <b>Available named values are</b>:<br>
     * int - yearly_salary<br>
     * int - no_people_in_household<br>
     * int - no_children_below_18<br>
     * int - net_monthly_household_income<br>
     * int - monthly_cost_accommodation<br>
     * int - monthly_cost_other_loans<br>
     *
     * Make sure you send in the values as the right data type.<br>
     * Use strval, intval or similar methods to ensure the right type is sent.
     *
     * @param string $name  key
     * @param mixed  $value value
     *
     * @throws KlarnaException
     * @return void
     */
    public function setIncomeInfo($name, $value)
    {
        $this->_checkArgument($name, "name");

        $this->incomeInfo[$name] = $value;
    }

    /**
     * Sets the bank information for the upcoming transaction.<br>
     *
     * Using this method is optional.
     *
     * <b>Available named values are</b>:<br>
     * int    - bank_acc_bic<br>
     * int    - bank_acc_no<br>
     * int    - bank_acc_pin<br>
     * int    - bank_acc_tan<br>
     * string - bank_name<br>
     * string - bank_city<br>
     * string - iban<br>
     *
     * Make sure you send in the values as the right data type.<br>
     * Use strval, intval or similar methods to ensure the right type is sent.
     *
     * @param string $name  key
     * @param mixed  $value value
     *
     * @throws KlarnaException
     * @return void
     */
    public function setBankInfo($name, $value)
    {
        $this->_checkArgument($name, "name");

        $this->bankInfo[$name] = $value;
    }

    /**
     * Sets the travel information for the upcoming transaction.<br>
     *
     * Using this method is optional.
     *
     * <b>Available named values are</b>:<br>
     * string - travel_company<br>
     * string - reseller_company<br>
     * string - departure_date<br>
     * string - return_date<br>
     * array  - destinations<br>
     * array  - passenger_list<br>
     * array  - passport_no<br>
     * array  - driver_license_no<br>
     *
     * Make sure you send in the values as the right data type.<br>
     * Use strval, intval or similar methods to ensure the right type is sent.
     *
     * @param string $name  key
     * @param mixed  $value value
     *
     * @throws KlarnaException
     * @return void
     */
    public function setTravelInfo($name, $value)
    {
        $this->_checkArgument($name, "name");

        $this->travelInfo[$name] = $value;
    }

    /**
     * Returns the clients IP address.
     *
     * @return string
     */
    public function getClientIP()
    {
        $tmp_ip = '';
        $x_fwd = null;

        //Proxy handling.
        if (array_key_exists('REMOTE_ADDR', $_SERVER)) {
            $tmp_ip = $_SERVER['REMOTE_ADDR'];
        }

        if (isset($_SERVER["HTTP_X_FORWARDED_FOR"])) {
            $x_fwd = $_SERVER["HTTP_X_FORWARDED_FOR"];
        }

        if (self::$x_forwarded_for && ($x_fwd !== null)) {
            $forwarded = explode(",", $x_fwd);
            return trim($forwarded[0]);
        }

        return $tmp_ip;
    }

    /**
     * Sets the specified address for the current order.
     *
     * <b>Address type can be</b>:<br>
     * {@link KlarnaFlags::IS_SHIPPING}<br>
     * {@link KlarnaFlags::IS_BILLING}<br>
     *
     * @param int        $type Address type.
     * @param KlarnaAddr $addr Specified address.
     *
     * @throws KlarnaException
     * @return void
     */
    public function setAddress($type, $addr)
    {
        if (!($addr instanceof KlarnaAddr)) {
            throw new Klarna_InvalidKlarnaAddrException;
        }

        if ($addr->isCompany === null) {
            $addr->isCompany = false;
        }

        if ($type === KlarnaFlags::IS_SHIPPING) {
            $this->shipping = $addr;
            self::printDebug("shipping address array", $this->shipping);
            return;
        }

        if ($type === KlarnaFlags::IS_BILLING) {
            $this->billing = $addr;
            self::printDebug("billing address array", $this->billing);
            return;
        }
        throw new Klarna_UnknownAddressTypeException($type);
    }

    /**
     * Sets order id's from other systems for the upcoming transaction.<br>
     * User is only sent with {@link Klarna::addTransaction()}.<br>
     *
     * @param string $orderid1 order id 1
     * @param string $orderid2 order id 2
     * @param string $user     username
     *
     * @see Klarna::setExtraInfo()
     *
     * @throws KlarnaException
     * @return void
     */
    public function setEstoreInfo($orderid1 = "", $orderid2 = "", $user = "")
    {
        if (!is_string($orderid1)) {
            $orderid1 = strval($orderid1);
        }

        if (!is_string($orderid2)) {
            $orderid2 = strval($orderid2);
        }

        if (!is_string($user)) {
            $user = strval($user);
        }

        if (strlen($user) > 0 ) {
            $this->setExtraInfo('estore_user', $user);
        }

        $this->orderid[0] = $orderid1;
        $this->orderid[1] = $orderid2;
    }

    /**
     * Sets the reference (person) and reference code, for the upcoming
     * transaction.
     *
     * If this is omitted, it can grab first name, last name from the address
     * and use that as a reference person.
     *
     * @param string $ref  Reference person / message to customer on invoice.
     * @param string $code Reference code / message to customer on invoice.
     *
     * @return void
     */
    public function setReference($ref, $code)
    {
        $this->_checkRef($ref, $code);
        $this->reference = $ref;
        $this->reference_code = $code;
    }

    /**
     * Returns the reference (person).
     *
     * @return string
     */
    public function getReference()
    {
        return $this->reference;
    }

    /**
     * Returns an associative array used to send the address to Klarna.
     * TODO: Kill it all
     *
     * @param KlarnaAddr $addr Address object to assemble.
     *
     * @throws KlarnaException
     * @return array The address for the specified method.
     */
    protected function assembleAddr($addr)
    {
        if (!($addr instanceof KlarnaAddr)) {
            throw new Klarna_InvalidKlarnaAddrException;
        }

        return $addr->toArray();
    }

    /**
     * Sets the comment field, which can be shown in the invoice.
     *
     * @param string $data comment to set
     *
     * @return void
     */
    public function setComment($data)
    {
        $this->comment = $data;
    }

    /**
     * Adds an additional comment to the comment field. Appends with a newline.
     *
     * @param string $data comment to add
     *
     * @see Klarna::setComment()
     *
     * @return void
     */
    public function addComment($data)
    {
        $this->comment .= "\n".$data;
    }

    /**
     * Returns the PNO/SSN encoding constant for currently set country.
     *
     * <b>Note</b>:<br>
     * Country, language and currency needs to match!
     *
     * @throws KlarnaException
     * @return int  {@link KlarnaEncoding} constant.
     */
    public function getPNOEncoding()
    {
        $this->_checkLocale();

        $country = KlarnaCountry::getCode($this->_country);

        return KlarnaEncoding::get($country);
    }

    /**
     * Purpose: The get_addresses function is used to retrieve a customer's
     * address(es). Using this, the customer is not required to enter any
     * information, only confirm the one presented to him/her.<br>
     *
     * The get_addresses function can also be used for companies.<br>
     * If the customer enters a company number, it will return all the
     * addresses where the company is registered at.<br>
     *
     * The get_addresses function is ONLY allowed to be used for Swedish
     * persons with the following conditions:
     * <ul>
     *     <li>
     *          It can be only used if invoice or part payment is
     *          the default payment method
     *     </li>
     *     <li>
     *          It has to disappear if the customer chooses another
     *          payment method
     *     </li>
     *     <li>
     *          The button is not allowed to be called "get address", but
     *          "continue" or<br>
     *          it can be picked up automatically when all the numbers have
     *          been typed.
     *     </li>
     * </ul>
     *
     * <b>Type can be one of these</b>:<br>
     * {@link KlarnaFlags::GA_ALL},<br>
     * {@link KlarnaFlags::GA_LAST},<br>
     * {@link KlarnaFlags::GA_GIVEN}.<br>
     *
     * @param string $pno      Social security number, personal number, ...
     * @param int    $encoding {@link KlarnaEncoding PNO Encoding} constant.
     * @param int    $type     Specifies returned information.
     *
     * @link http://integration.klarna.com/en/api/standard-integration/functions
     *       /getaddresses
     * @throws KlarnaException
     * @return array   An array of {@link KlarnaAddr} objects.
     */
    public function getAddresses(
        $pno, $encoding = null, $type = KlarnaFlags::GA_GIVEN
    ) {
        if ($this->_country !== KlarnaCountry::SE) {
            throw new Klarna_UnsupportedMarketException("Sweden");
        }

        //Get the PNO/SSN encoding constant.
        if ($encoding === null) {
            $encoding = $this->getPNOEncoding();
        }

        $this->_checkPNO($pno, $encoding);

        $digestSecret = self::digest(
            $this->colon(
                $this->_eid, $pno, $this->_secret
            )
        );

        $paramList = array(
            $pno,
            $this->_eid,
            $digestSecret,
            $encoding,
            $type,
            $this->getClientIP()
        );

        self::printDebug("get_addresses array", $paramList);

        $result = $this->xmlrpc_call('get_addresses', $paramList);

        self::printDebug("get_addresses result array", $result);

        $addrs = array();
        foreach ($result as $tmpAddr) {
            try {
                $addr = new KlarnaAddr();
                if ($type === KlarnaFlags::GA_GIVEN) {
                    $addr->isCompany = (count($tmpAddr) == 5) ? true : false;
                    if ($addr->isCompany) {
                        $addr->setCompanyName($tmpAddr[0]);
                        $addr->setStreet($tmpAddr[1]);
                        $addr->setZipCode($tmpAddr[2]);
                        $addr->setCity($tmpAddr[3]);
                        $addr->setCountry($tmpAddr[4]);
                    } else {
                        $addr->setFirstName($tmpAddr[0]);
                        $addr->setLastName($tmpAddr[1]);
                        $addr->setStreet($tmpAddr[2]);
                        $addr->setZipCode($tmpAddr[3]);
                        $addr->setCity($tmpAddr[4]);
                        $addr->setCountry($tmpAddr[5]);
                    }
                } else if ($type === KlarnaFlags::GA_LAST) {
                    // Here we cannot decide if it is a company or not?
                    // Assume private person.
                    $addr->setLastName($tmpAddr[0]);
                    $addr->setStreet($tmpAddr[1]);
                    $addr->setZipCode($tmpAddr[2]);
                    $addr->setCity($tmpAddr[3]);
                    $addr->setCountry($tmpAddr[4]);
                } else if ($type === KlarnaFlags::GA_ALL) {
                    if (strlen($tmpAddr[0]) > 0) {
                        $addr->setFirstName($tmpAddr[0]);
                        $addr->setLastName($tmpAddr[1]);
                    } else {
                        $addr->isCompany = true;
                        $addr->setCompanyName($tmpAddr[1]);
                    }
                    $addr->setStreet($tmpAddr[2]);
                    $addr->setZipCode($tmpAddr[3]);
                    $addr->setCity($tmpAddr[4]);
                    $addr->setCountry($tmpAddr[5]);
                } else {
                    continue;
                }
                $addrs[] = $addr;
            } catch(Exception $e) {
                //Silently fail
            }
        }

        return $addrs;
    }

    /**
     * Adds an article to the current goods list for the current order.
     *
     * <b>Note</b>:<br>
     * It is recommended that you use {@link KlarnaFlags::INC_VAT}.<br>
     *
     * <b>Flags can be</b>:<br>
     * {@link KlarnaFlags::INC_VAT}<br>
     * {@link KlarnaFlags::IS_SHIPMENT}<br>
     * {@link KlarnaFlags::IS_HANDLING}<br>
     * {@link KlarnaFlags::PRINT_1000}<br>
     * {@link KlarnaFlags::PRINT_100}<br>
     * {@link KlarnaFlags::PRINT_10}<br>
     * {@link KlarnaFlags::NO_FLAG}<br>
     *
     * Some flags can be added to each other for multiple options.
     *
     * @param int    $qty      Quantity.
     * @param string $artNo    Article number.
     * @param string $title    Article title.
     * @param int    $price    Article price.
     * @param float  $vat      VAT in percent, e.g. 25% is inputted as 25.
     * @param float  $discount Possible discount on article.
     * @param int    $flags    Options which specify the article
     *                         ({@link KlarnaFlags::IS_HANDLING}) and it's price
     *                         ({@link KlarnaFlags::INC_VAT})
     *
     * @see Klarna::addTransaction()
     * @see Klarna::reserveAmount()
     * @see Klarna::activateReservation()
     *
     * @throws KlarnaException
     * @return void
     */
    public function addArticle(
        $qty, $artNo, $title, $price, $vat, $discount = 0,
        $flags = KlarnaFlags::INC_VAT
    ) {
        $this->_checkQty($qty);

        // Either artno or title has to be set
        if ((($artNo === null ) || ($artNo == ""))
            && (($title === null ) || ($title == ""))
        ) {
            throw new Klarna_ArgumentNotSetException('Title and ArtNo', 50026);
        }

        $this->_checkPrice($price);
        $this->_checkVAT($vat);
        $this->_checkDiscount($discount);
        $this->_checkInt($flags, 'flags');

        //Create goodsList array if not set.
        if (!$this->goodsList || !is_array($this->goodsList)) {
            $this->goodsList = array();
        }

        //Populate a temp array with the article details.
        $tmpArr = array(
            "artno" => $artNo,
            "title" => $title,
            "price" => $price,
            "vat" => $vat,
            "discount" => $discount,
            "flags" => $flags
        );

        //Add the temp array and quantity field to the internal goods list.
        $this->goodsList[] = array(
                "goods" => $tmpArr,
                "qty"   => $qty
        );

        if (count($this->goodsList) > 0) {
            self::printDebug(
                "article added",
                $this->goodsList[count($this->goodsList)-1]
            );
        }
    }

    /**
     * Assembles and sends the current order to Klarna.<br>
     * This clears all relevant data if $clear is set to true.<br>
     *
     * <b>This method returns an array with</b>:<br>
     * Invoice number<br>
     * Order status flag<br>
     *
     * If the flag {@link KlarnaFlags::RETURN_OCR} is used:<br>
     * Invoice number<br>
     * OCR number <br>
     * Order status flag<br>
     *
     * <b>Order status can be</b>:<br>
     * {@link KlarnaFlags::ACCEPTED}<br>
     * {@link KlarnaFlags::PENDING}<br>
     * {@link KlarnaFlags::DENIED}<br>
     *
     * Gender is only required for Germany and Netherlands.<br>
     *
     * <b>Flags can be</b>:<br>
     * {@link KlarnaFlags::NO_FLAG}<br>
     * {@link KlarnaFlags::TEST_MODE}<br>
     * {@link KlarnaFlags::AUTO_ACTIVATE}<br>
     * {@link KlarnaFlags::SENSITIVE_ORDER}<br>
     * {@link KlarnaFlags::RETURN_OCR}<br>
     * {@link KlarnaFlags::M_PHONE_TRANSACTION}<br>
     * {@link KlarnaFlags::M_SEND_PHONE_PIN}<br>
     *
     * Some flags can be added to each other for multiple options.
     *
     * <b>Note</b>:<br>
     * Normal shipment type is assumed unless otherwise specified,
     * ou can do this by calling:<br>
     * {@link Klarna::setShipmentInfo() setShipmentInfo('delay_adjust', ...)}
     * with either:<br>
     * {@link KlarnaFlags::NORMAL_SHIPMENT NORMAL_SHIPMENT} or
     * {@link KlarnaFlags::EXPRESS_SHIPMENT EXPRESS_SHIPMENT}<br>
     *
     * @param string $pno      Personal number, SSN, date of birth, etc.
     * @param int    $gender   {@link KlarnaFlags::FEMALE} or
     *                         {@link KlarnaFlags::MALE},
     *                         null or "" for unspecified.
     * @param int    $flags    Options which affect the behaviour.
     * @param int    $pclass   PClass id used for this invoice.
     * @param int    $encoding {@link KlarnaEncoding Encoding} constant for the
     *                         PNO parameter.
     * @param bool   $clear    Whether customer info should be cleared after
     *                         this call or not.
     *
     * @link http://integration.klarna.com/en/api/standard-integration/functions/
     *       addtransaction
     *
     * @throws KlarnaException
     * @return array An array with invoice number and order status. [string, int]
     */
    public function addTransaction(
        $pno, $gender, $flags = KlarnaFlags::NO_FLAG,
        $pclass = KlarnaPClass::INVOICE, $encoding = null, $clear = true
    ) {
        $this->_checkLocale(50023);

        //Get the PNO/SSN encoding constant.
        if ($encoding === null) {
            $encoding = $this->getPNOEncoding();
        }

        if (!($flags & KlarnaFlags::PRE_PAY)) {
            $this->_checkPNO($pno, $encoding);
        }

        if ($gender === 'm') {
            $gender = KlarnaFlags::MALE;
        } else if ($gender === 'f') {
            $gender = KlarnaFlags::FEMALE;
        }

        if ($gender !== null && strlen($gender) > 0) {
            $this->_checkInt($gender, 'gender');
        }

        $this->_checkInt($flags,  'flags');
        $this->_checkInt($pclass, 'pclass');

        //Check so required information is set.
        $this->_checkGoodslist();

        //We need at least one address set
        if (!($this->billing instanceof KlarnaAddr)
            && !($this->shipping instanceof KlarnaAddr)
        ) {
            throw new Klarna_MissingAddressException;
        }

        //If only one address is set, copy to the other address.
        if (!($this->shipping instanceof KlarnaAddr)
            && ($this->billing instanceof KlarnaAddr)
        ) {
            $this->shipping = $this->billing;
        } else if (!($this->billing instanceof KlarnaAddr)
            && ($this->shipping instanceof KlarnaAddr)
        ) {
            $this->billing = $this->shipping;
        }

        //Assume normal shipment unless otherwise specified.
        if (!isset($this->shipInfo['delay_adjust'])) {
            $this->setShipmentInfo('delay_adjust', KlarnaFlags::NORMAL_SHIPMENT);
        }

        //Make sure we get any session ID's or similar
        $this->initCheckout();

        //function add_transaction_digest
        $string = "";
        foreach ($this->goodsList as $goods) {
            $string .= $goods['goods']['title'] .':';
        }
        $digestSecret = self::digest($string . $this->_secret);
        //end function add_transaction_digest

        $billing = $this->assembleAddr($this->billing);
        $shipping = $this->assembleAddr($this->shipping);

        //Shipping country must match specified country!
        if (strlen($shipping['country']) > 0
            && ($shipping['country'] !== $this->_country)
        ) {
            throw new Klarna_ShippingCountryException;
        }

        $paramList = array(
            $pno,
            $gender,
            $this->reference,
            $this->reference_code,
            $this->orderid[0],
            $this->orderid[1],
            $shipping,
            $billing,
            $this->getClientIP(),
            $flags,
            $this->_currency,
            $this->_country,
            $this->_language,
            $this->_eid,
            $digestSecret,
            $encoding,
            $pclass,
            $this->goodsList,
            $this->comment,
            $this->shipInfo,
            $this->travelInfo,
            $this->incomeInfo,
            $this->bankInfo,
            $this->sid,
            $this->extraInfo
        );

        self::printDebug('add_invoice', $paramList);

        $result = $this->xmlrpc_call('add_invoice', $paramList);

        if ($clear === true) {
            //Make sure any stored values that need to be unique between
            //purchases are cleared.
            foreach ($this->coObjects as $co) {
                $co->clear();
            }
            $this->clear();
        }

        self::printDebug('add_invoice result', $result);

        return $result;
    }


    /**
     * Activates previously created invoice
     * (from {@link Klarna::addTransaction()}).
     *
     * <b>Note</b>:<br>
     * If you want to change the shipment type, you can specify it using:
     * {@link Klarna::setShipmentInfo() setShipmentInfo('delay_adjust', ...)}
     * with either: {@link KlarnaFlags::NORMAL_SHIPMENT NORMAL_SHIPMENT} or
     * {@link KlarnaFlags::EXPRESS_SHIPMENT EXPRESS_SHIPMENT}
     *
     * @param string $invNo  Invoice number.
     * @param int    $pclass PClass id used for this invoice.
     * @param bool   $clear  Whether customer info should be cleared after this
     *                       call.
     *
     * @see Klarna::setShipmentInfo()
     * @link http://integration.klarna.com/en/api/standard-integration/functions
     *       /activateinvoice
     *
     * @throws KlarnaException
     * @return string  An URL to the PDF invoice.
     */
    public function activateInvoice(
        $invNo, $pclass = KlarnaPClass::INVOICE, $clear = true
    ) {
        $this->_checkInvNo($invNo);

        $digestSecret = self::digest(
            $this->colon($this->_eid, $invNo, $this->_secret)
        );

        $paramList = array(
            $this->_eid,
            $invNo,
            $digestSecret,
            $pclass,
            $this->shipInfo
        );

        self::printDebug('activate_invoice', $paramList);

        $result = $this->xmlrpc_call('activate_invoice', $paramList);

        if ($clear === true) {
            $this->clear();
        }

        self::printDebug('activate_invoice result', $result);

        return $result;
    }

    /**
     * Removes a passive invoices which has previously been created with
     * {@link Klarna::addTransaction()}.
     * True is returned if the invoice was successfully removed, otherwise an
     * exception is thrown.<br>
     *
     * @param string $invNo Invoice number.
     *
     * @throws KlarnaException
     * @return bool
     */
    public function deleteInvoice($invNo)
    {
        $this->_checkInvNo($invNo);

        $digestSecret = self::digest(
            $this->colon($this->_eid, $invNo, $this->_secret)
        );

        $paramList = array(
            $this->_eid,
            $invNo,
            $digestSecret
        );

        self::printDebug('delete_invoice', $paramList);

        $result = $this->xmlrpc_call('delete_invoice', $paramList);

        return ($result == 'ok') ? true : false;
    }

    /**
     * Summarizes the prices of the held goods list
     *
     * @return int total amount
     */
    public function summarizeGoodsList()
    {
        $amount = 0;
        if (!is_array($this->goodsList)) {
            return $amount;
        }
        foreach ($this->goodsList as $goods) {
            $price = $goods['goods']['price'];

            // Add VAT if price is Excluding VAT
            if (($goods['goods']['flags'] & KlarnaFlags::INC_VAT) === 0) {
                $vat = $goods['goods']['vat'] / 100.0;
                $price *= (1.0 + $vat);
            }

            // Reduce discounts
            if ($goods['goods']['discount'] > 0) {
                $discount = $goods['goods']['discount'] / 100.0;
                $price *= (1.0 - $discount);
            }

            $amount += $price * (int)$goods['qty'];
        }
        return $amount;
    }

    /**
     * Reserves a purchase amount for a specific customer. <br>
     * The reservation is valid, by default, for 7 days.<br>
     *
     * <b>This method returns an array with</b>:<br>
     * A reservation number (rno)<br>
     * Order status flag<br>
     *
     * <b>Order status can be</b>:<br>
     * {@link KlarnaFlags::ACCEPTED}<br>
     * {@link KlarnaFlags::PENDING}<br>
     * {@link KlarnaFlags::DENIED}<br>
     *
     * <b>Please note</b>:<br>
     * Activation must be done with activate_reservation, i.e. you cannot
     * activate through Klarna Online.
     *
     * Gender is only required for Germany and Netherlands.<br>
     *
     * <b>Flags can be set to</b>:<br>
     * {@link KlarnaFlags::NO_FLAG}<br>
     * {@link KlarnaFlags::TEST_MODE}<br>
     * {@link KlarnaFlags::RSRV_SENSITIVE_ORDER}<br>
     * {@link KlarnaFlags::RSRV_PHONE_TRANSACTION}<br>
     * {@link KlarnaFlags::RSRV_SEND_PHONE_PIN}<br>
     *
     * Some flags can be added to each other for multiple options.
     *
     * <b>Note</b>:<br>
     * Normal shipment type is assumed unless otherwise specified, you can do
     * this by calling:<br>
     * {@link Klarna::setShipmentInfo() setShipmentInfo('delay_adjust', ...)}
     * with either: {@link KlarnaFlags::NORMAL_SHIPMENT NORMAL_SHIPMENT} or
     * {@link KlarnaFlags::EXPRESS_SHIPMENT EXPRESS_SHIPMENT}<br>
     *
     * @param string $pno      Personal number, SSN, date of birth, etc.
     * @param int    $gender   {@link KlarnaFlags::FEMALE} or
     *                         {@link KlarnaFlags::MALE}, null for unspecified.
     * @param int    $amount   Amount to be reserved, including VAT.
     * @param int    $flags    Options which affect the behaviour.
     * @param int    $pclass   {@link KlarnaPClass::getId() PClass ID}.
     * @param int    $encoding {@link KlarnaEncoding PNO Encoding} constant.
     * @param bool   $clear    Whether customer info should be cleared after
     *                         this call.
     *
     * @link http://integration.klarna.com/en/api/advanced-integration
     *       /functions/reserveamount
     *
     * @throws KlarnaException
     * @return array An array with reservation number and order
     *               status. [string, int]
     */
    public function reserveAmount(
        $pno, $gender, $amount, $flags = 0, $pclass = KlarnaPClass::INVOICE,
        $encoding = null, $clear = true
    ) {
        $this->_checkLocale();

        //Get the PNO/SSN encoding constant.
        if ($encoding === null) {
            $encoding = $this->getPNOEncoding();
        }

        $this->_checkPNO($pno, $encoding);

        if ($gender === 'm') {
            $gender = KlarnaFlags::MALE;
        } else if ($gender === 'f') {
            $gender = KlarnaFlags::FEMALE;
        }
        if ($gender !== null && strlen($gender) > 0) {
            $this->_checkInt($gender, 'gender');
        }

        $this->_checkInt($flags,  'flags');
        $this->_checkInt($pclass, 'pclass');

        //Check so required information is set.
        $this->_checkGoodslist();


        //Calculate automatically the amount from goodsList.
        if ($amount === -1) {
            $amount = (int)round($this->summarizeGoodsList());
        } else {
            $this->_checkAmount($amount);
        }

        if ($amount < 0) {
            throw new Klarna_InvalidPriceException($amount);
        }

        //No addresses used for phone transactions
        if ($flags & KlarnaFlags::RSRV_PHONE_TRANSACTION) {
            $billing = $shipping = '';
        } else {
            $billing = $this->assembleAddr($this->billing);
            $shipping = $this->assembleAddr($this->shipping);

            if (strlen($shipping['country']) > 0
                && ($shipping['country'] !== $this->_country)
            ) {
                throw new Klarna_ShippingCountryException;
            }
        }

        //Assume normal shipment unless otherwise specified.
        if (!isset($this->shipInfo['delay_adjust'])) {
            $this->setShipmentInfo('delay_adjust', KlarnaFlags::NORMAL_SHIPMENT);
        }

        //Make sure we get any session ID's or similar
        $this->initCheckout($this, $this->_eid);

        $digestSecret = self::digest(
            "{$this->_eid}:{$pno}:{$amount}:{$this->_secret}"
        );

        $paramList = array(
            $pno,
            $gender,
            $amount,
            $this->reference,
            $this->reference_code,
            $this->orderid[0],
            $this->orderid[1],
            $shipping,
            $billing,
            $this->getClientIP(),
            $flags,
            $this->_currency,
            $this->_country,
            $this->_language,
            $this->_eid,
            $digestSecret,
            $encoding, $pclass,
            $this->goodsList,
            $this->comment,
            $this->shipInfo,
            $this->travelInfo,
            $this->incomeInfo,
            $this->bankInfo,
            $this->sid,
            $this->extraInfo
        );

        self::printDebug('reserve_amount', $paramList);

        $result = $this->xmlrpc_call('reserve_amount', $paramList);

        if ($clear === true) {
            //Make sure any stored values that need to be unique between
            //purchases are cleared.
            foreach ($this->coObjects as $co) {
                $co->clear();
            }
            $this->clear();
        }

        self::printDebug('reserve_amount result', $result);

        return $result;
    }

    /**
     * Cancels a reservation.
     *
     * @param string $rno Reservation number.
     *
     * @link http://integration.klarna.com/en/api/advanced-integration/functions
     *       /cancelreservation
     *
     * @throws KlarnaException
     * @return bool True, if the cancellation was successful.
     */
    public function cancelReservation($rno)
    {
        $this->_checkRNO($rno);

        $digestSecret = self::digest(
            $this->colon($this->_eid, $rno, $this->_secret)
        );
        $paramList = array(
            $rno,
            $this->_eid,
            $digestSecret
        );

        self::printDebug('cancel_reservation', $paramList);

        $result = $this->xmlrpc_call('cancel_reservation', $paramList);

        return ($result == 'ok');
    }

    /**
     * Changes specified reservation to a new amount.
     *
     * <b>Flags can be either of these</b>:<br>
     * {@link KlarnaFlags::NEW_AMOUNT}<br>
     * {@link KlarnaFlags::ADD_AMOUNT}<br>
     *
     * @param string $rno    Reservation number.
     * @param int    $amount Amount including VAT.
     * @param int    $flags  Options which affect the behaviour.
     *
     * @link http://integration.klarna.com/en/api/advanced-integration/functions
     *       /changereservation
     *
     * @throws KlarnaException
     * @return bool    True, if the change was successful.
     */
    public function changeReservation(
        $rno, $amount, $flags = KlarnaFlags::NEW_AMOUNT
    ) {
        $this->_checkRNO($rno);
        $this->_checkAmount($amount);
        $this->_checkInt($flags, 'flags');

        $digestSecret = self::digest(
            $this->colon($this->_eid, $rno, $amount, $this->_secret)
        );
        $paramList = array(
            $rno,
            $amount,
            $this->_eid,
            $digestSecret,
            $flags
        );

        self::printDebug('change_reservation', $paramList);

        $result = $this->xmlrpc_call('change_reservation', $paramList);

        return ($result  == 'ok') ? true : false;
    }

    /**
     * Update the reservation matching the given reservation number.
     *
     * @param string  $rno   Reservation number
     * @param boolean $clear clear set data aftre updating. Defaulted to true.
     *
     * @throws KlarnaException if no RNO is given, or if an error is recieved
     *         from Klarna Online.
     *
     * @return true if the update was successful
     */
    public function update($rno, $clear = true)
    {
        $rno = strval($rno);

        // All info that is sent in is part of the digest secret, in this order:
        // [
        //      proto_vsn, client_vsn, eid, rno, careof, street, zip, city,
        //      country, fname, lname, careof, street, zip, city, country,
        //      fname, lname, artno, qty, orderid1, orderid2
        // ].
        // The address part appears twice, that is one per address that
        // changes. If no value is sent in for an optional field, there
        // is no entry for this field in the digest secret. Shared secret
        // is added at the end of the digest secret.
        $digestArray = array(
            str_replace('.', ':', $this->PROTO),
            $this->VERSION,
            $this->_eid,
            $rno
        );
        $digestArray = array_merge(
            $digestArray, $this->_addressDigestPart($this->shipping)
        );
        $digestArray = array_merge(
            $digestArray, $this->_addressDigestPart($this->billing)
        );
        if (is_array($this->goodsList) && $this->goodsList !== array()) {
            foreach ($this->goodsList as $goods) {
                if (strlen($goods["goods"]["artno"]) > 0) {
                    $digestArray[] = $goods["goods"]["artno"];
                } else {
                    $digestArray[] = $goods["goods"]["title"];
                }
                $digestArray[] = $goods["qty"];
            }
        }
        foreach ($this->orderid as $orderid) {
            $digestArray[] = $orderid;
        }
        $digestArray[] = $this->_secret;

        $digestSecret = $this->digest(
            call_user_func_array(
                array('self', 'colon'), $digestArray
            )
        );

        $shipping = array();
        $billing = array();
        if ($this->shipping !== null && $this->shipping instanceof KlarnaAddr) {
            $shipping = $this->shipping->toArray();
        }
        if ($this->billing !== null && $this->billing instanceof KlarnaAddr) {
            $billing = $this->billing->toArray();
        }
        $paramList = array(
            $this->_eid,
            $digestSecret,
            $rno,
            array(
                'goods_list' => $this->goodsList,
                'dlv_addr' => $shipping,
                'bill_addr' => $billing,
                'orderid1' => $this->orderid[0],
                'orderid2' => $this->orderid[1]
            )
        );

        self::printDebug('update array', $paramList);

        $result = $this->xmlrpc_call('update', $paramList);

        self::printDebug('update result', $result);

        return ($result === 'ok');
    }

    /**
     * Help function to sort the address for update digest.
     *
     * @param KlarnaAddr|null $address KlarnaAddr object or null
     *
     * @return array
     */
    private function _addressDigestPart(KlarnaAddr $address = null)
    {
        if ($address === null) {
            return array();
        }

        $keyOrder = array(
            'careof', 'street', 'zip', 'city', 'country', 'fname', 'lname'
        );

        $holder = $address->toArray();
        $digest = array();

        foreach ($keyOrder as $key) {
            if ($holder[$key] != "") {
                $digest[] = $holder[$key];
            }
        }

        return $digest;
    }

    /**
     * Activate the reservation matching the given reservation number.
     * Optional information should be set in ActivateInfo.
     *
     * To perform a partial activation, use the addArtNo function to specify
     * which items in the reservation to include in the activation.
     *
     * @param string  $rno   Reservation number
     * @param string  $ocr   optional OCR number to attach to the reservation when
     *                       activating. Overrides OCR specified in activateInfo.
     * @param string  $flags optional flags to affect behavior. If specified it
     *                       will overwrite any flag set in activateInfo.
     * @param boolean $clear clear set data after activating. Defaulted to true.
     *
     * @throws KlarnaException when the RNO is not specified, or if an error
     *         is recieved from Klarna Online.
     * @return A string array with risk status and reservation number.
     */
    public function activate(
        $rno, $ocr = null, $flags = null, $clear = true
    ) {
        $this->_checkRNO($rno);

        // Overwrite any OCR set on activateInfo if supplied here since this
        // method call is more specific.
        if ($ocr !== null) {
            $this->setActivateInfo('ocr', $ocr);
        }

        // If flags is specified set the flag supplied here to activateInfo.
        if ($flags !== null) {
            $this->setActivateInfo('flags', $flags);
        }

        //Assume normal shipment unless otherwise specified.
        if (!array_key_exists('delay_adjust', $this->shipInfo)) {
            $this->setShipmentInfo('delay_adjust', KlarnaFlags::NORMAL_SHIPMENT);
        }

        // Append shipment info to activateInfo
        $this->activateInfo['shipment_info'] = $this->shipInfo;

        // Unlike other calls, if NO_FLAG is specified it should not be sent in
        // at all.
        if (array_key_exists('flags', $this->activateInfo)
            && $this->activateInfo['flags'] === KlarnaFlags::NO_FLAG
        ) {
            unset($this->activateInfo['flags']);
        }

        // Build digest. Any field in activateInfo that is set is included in
        // the digest.
        $digestArray = array(
            str_replace('.', ':', $this->PROTO),
            $this->VERSION,
            $this->_eid,
            $rno
        );

        $optionalDigestKeys = array(
            'bclass',
            'cust_no',
            'flags',
            'ocr',
            'orderid1',
            'orderid2',
            'reference',
            'reference_code'
        );

        foreach ($optionalDigestKeys as $key) {
            if (array_key_exists($key, $this->activateInfo)) {
                $digestArray[] = $this->activateInfo[$key];
            }
        }

        if (array_key_exists('delay_adjust', $this->activateInfo['shipment_info'])) {
            $digestArray[] = $this->activateInfo['shipment_info']['delay_adjust'];
        }

        // If there are any artnos added with addArtNo, add them to the digest
        // and to the activateInfo
        if (is_array($this->artNos)) {
            foreach ($this->artNos as $artNo) {
                $digestArray[] = $artNo['artno'];
                $digestArray[] = $artNo['qty'];
            }
            $this->setActivateInfo('artnos', $this->artNos);
        }

        $digestArray[] = $this->_secret;
        $digestSecret = self::digest(
            call_user_func_array(
                array('self', 'colon'), $digestArray
            )
        );

        // Create the parameter list.
        $paramList = array(
            $this->_eid,
            $digestSecret,
            $rno,
            $this->activateInfo
        );

        self::printDebug('activate array', $paramList);

        $result = $this->xmlrpc_call('activate', $paramList);

        self::printDebug('activate result', $result);

        // Clear the state if specified.
        if ($clear) {
            $this->clear();
        }

        return $result;
    }

    /**
     * Activates a previously created reservation.
     *
     * <b>This method returns an array with</b>:<br>
     * Risk status ("no_risk", "ok")<br>
     * Invoice number<br>
     *
     * Gender is only required for Germany and Netherlands.<br>
     *
     * Use of the OCR parameter is optional.
     * An OCR number can be retrieved by using:
     * {@link Klarna::reserveOCR()} or {@link Klarna::reserveOCRemail()}.
     *
     * <b>Flags can be set to</b>:<br>
     * {@link KlarnaFlags::NO_FLAG}<br>
     * {@link KlarnaFlags::TEST_MODE}<br>
     * {@link KlarnaFlags::RSRV_SEND_BY_MAIL}<br>
     * {@link KlarnaFlags::RSRV_SEND_BY_EMAIL}<br>
     * {@link KlarnaFlags::RSRV_PRESERVE_RESERVATION}<br>
     * {@link KlarnaFlags::RSRV_SENSITIVE_ORDER}<br>
     *
     * Some flags can be added to each other for multiple options.
     *
     * <b>Note</b>:<br>
     * Normal shipment type is assumed unless otherwise specified, you can
     * do this by calling:
     * {@link Klarna::setShipmentInfo() setShipmentInfo('delay_adjust', ...)}
     * with either: {@link KlarnaFlags::NORMAL_SHIPMENT NORMAL_SHIPMENT} or
     * {@link KlarnaFlags::EXPRESS_SHIPMENT EXPRESS_SHIPMENT}<br>
     *
     * @param string $pno      Personal number, SSN, date of birth, etc.
     * @param string $rno      Reservation number.
     * @param int    $gender   {@link KlarnaFlags::FEMALE} or
     *                         {@link KlarnaFlags::MALE}, null for unspecified.
     * @param string $ocr      A OCR number.
     * @param int    $flags    Options which affect the behaviour.
     * @param int    $pclass   {@link KlarnaPClass::getId() PClass ID}.
     * @param int    $encoding {@link KlarnaEncoding PNO Encoding} constant.
     * @param bool   $clear    Whether customer info should be cleared after
     *                         this call.
     *
     * @link http://integration.klarna.com/en/api/advanced-integration/functions
     *       /activatereservation
     * @see Klarna::reserveAmount()
     *
     * @throws KlarnaException
     * @return array An array with risk status and invoice number [string, string].
     */
    public function activateReservation(
        $pno, $rno, $gender, $ocr = "", $flags = KlarnaFlags::NO_FLAG,
        $pclass = KlarnaPClass::INVOICE, $encoding = null, $clear = true
    ) {
        $this->_checkLocale();

        //Get the PNO/SSN encoding constant.
        if ($encoding === null) {
            $encoding = $this->getPNOEncoding();
        }

        // Only check PNO if it is not explicitly null.
        if ($pno !== null) {
            $this->_checkPNO($pno, $encoding);
        }

        $this->_checkRNO($rno);

        if ($gender !== null && strlen($gender) > 0) {
            $this->_checkInt($gender, 'gender');
        }

        $this->_checkOCR($ocr);
        $this->_checkRef($this->reference, $this->reference_code);

        $this->_checkGoodslist();

        //No addresses used for phone transactions
        $billing = $shipping = '';
        if ( !($flags & KlarnaFlags::RSRV_PHONE_TRANSACTION) ) {
            $billing = $this->assembleAddr($this->billing);
            $shipping = $this->assembleAddr($this->shipping);

            if (strlen($shipping['country']) > 0
                && ($shipping['country'] !== $this->_country)
            ) {
                throw new Klarna_ShippingCountryException;
            }
        }

        //activate digest
        $string = $this->_eid . ":" . $pno . ":";
        foreach ($this->goodsList as $goods) {
            $string .= $goods["goods"]["artno"] . ":" . $goods["qty"] . ":";
        }
        $digestSecret = self::digest($string . $this->_secret);
        //end digest

        //Assume normal shipment unless otherwise specified.
        if (!isset($this->shipInfo['delay_adjust'])) {
            $this->setShipmentInfo('delay_adjust', KlarnaFlags::NORMAL_SHIPMENT);
        }

        $paramList = array(
            $rno,
            $ocr,
            $pno,
            $gender,
            $this->reference,
            $this->reference_code,
            $this->orderid[0],
            $this->orderid[1],
            $shipping,
            $billing,
            "0.0.0.0",
            $flags,
            $this->_currency,
            $this->_country,
            $this->_language,
            $this->_eid,
            $digestSecret,
            $encoding,
            $pclass,
            $this->goodsList,
            $this->comment,
            $this->shipInfo,
            $this->travelInfo,
            $this->incomeInfo,
            $this->bankInfo,
            $this->extraInfo
        );

        self::printDebug('activate_reservation', $paramList);

        $result = $this->xmlrpc_call('activate_reservation', $paramList);

        if ($clear === true) {
            $this->clear();
        }

        self::printDebug('activate_reservation result', $result);

        return $result;
    }


    /**
     * Splits a reservation due to for example outstanding articles.
     *
     * <b>For flags usage see</b>:<br>
     * {@link Klarna::reserveAmount()}<br>
     *
     * @param string $rno    Reservation number.
     * @param int    $amount The amount to be subtracted from the reservation.
     * @param int    $flags  Options which affect the behaviour.
     *
     * @link http://integration.klarna.com/en/api/advanced-integration/functions
     *       /splitreservation
     *
     * @throws KlarnaException
     * @return string A new reservation number.
     */
    public function splitReservation(
        $rno, $amount, $flags = KlarnaFlags::NO_FLAG
    ) {
        //Check so required information is set.
        $this->_checkRNO($rno);
        $this->_checkAmount($amount);

        if ($amount <= 0) {
            throw new Klarna_InvalidPriceException($amount);
        }

        $digestSecret = self::digest(
            $this->colon($this->_eid, $rno, $amount, $this->_secret)
        );
        $paramList = array(
            $rno,
            $amount,
            $this->orderid[0],
            $this->orderid[1],
            $flags,
            $this->_eid,
            $digestSecret
        );

        self::printDebug('split_reservation array', $paramList);

        $result = $this->xmlrpc_call('split_reservation', $paramList);

        self::printDebug('split_reservation result', $result);

        return $result;
    }

    /**
     * Reserves a specified number of OCR numbers.<br>
     * For the specified country or the {@link Klarna::setCountry() set country}.<br>
     *
     * @param int $no      The number of OCR numbers to reserve.
     * @param int $country {@link KlarnaCountry} constant.
     *
     * @link http://integration.klarna.com/en/api/advanced-integration/functions
     *       /reserveocrnums
     *
     * @throws KlarnaException
     * @return array An array of OCR numbers.
     */
    public function reserveOCR($no, $country = null)
    {
        $this->_checkNo($no);
        if ($country === null) {
            if (!$this->_country) {
                throw new Klarna_MissingCountryException;
            }
            $country = $this->_country;
        } else {
            $this->_checkCountry($country);
        }

        $digestSecret = self::digest(
            $this->colon($this->_eid, $no, $this->_secret)
        );
        $paramList = array(
            $no,
            $this->_eid,
            $digestSecret,
            $country
        );

        self::printDebug('reserve_ocr_nums array', $paramList);

        return $this->xmlrpc_call('reserve_ocr_nums', $paramList);
    }

    /**
     * Reserves the number of OCRs specified and sends them to the given email.
     *
     * @param int    $no      Number of OCR numbers to reserve.
     * @param string $email   address.
     * @param int    $country {@link KlarnaCountry} constant.
     *
     * @return bool True, if the OCRs were reserved and sent.
     */
    public function reserveOCRemail($no, $email, $country = null)
    {
        $this->_checkNo($no);
        $this->_checkPNO($email, KlarnaEncoding::EMAIL);

        if ($country === null) {
            if (!$this->_country) {
                throw new Klarna_MissingCountryException;
            }
            $country = $this->_country;
        } else {
            $this->_checkCountry($country);
        }

        $digestSecret = self::digest(
            $this->colon($this->_eid, $no, $this->_secret)
        );
        $paramList = array(
            $no,
            $email,
            $this->_eid,
            $digestSecret,
            $country
        );

        self::printDebug('reserve_ocr_nums_email array', $paramList);

        $result = $this->xmlrpc_call('reserve_ocr_nums_email', $paramList);

        return ($result == 'ok');
    }

    /**
     * Checks if the specified SSN/PNO has an part payment account with Klarna.
     *
     * @param string $pno      Social security number, Personal number, ...
     * @param int    $encoding {@link KlarnaEncoding PNO Encoding} constant.
     *
     * @link http://integration.klarna.com/en/api/standard-integration/functions
     *       /hasaccount
     *
     * @throws KlarnaException
     * @return bool    True, if customer has an account.
     */
    public function hasAccount($pno, $encoding = null)
    {
        //Get the PNO/SSN encoding constant.
        if ($encoding === null) {
            $encoding = $this->getPNOEncoding();
        }

        $this->_checkPNO($pno, $encoding);

        $digest = self::digest(
            $this->colon($this->_eid, $pno, $this->_secret)
        );

        $paramList = array(
            $this->_eid,
            $pno,
            $digest,
            $encoding
        );

        self::printDebug('has_account', $paramList);

        $result = $this->xmlrpc_call('has_account', $paramList);

        return ($result === 'true');
    }

    /**
     * Adds an article number and quantity to be used in
     * {@link Klarna::activatePart()}, {@link Klarna::creditPart()}
     * and {@link Klarna::invoicePartAmount()}.
     *
     * @param int    $qty   Quantity of specified article.
     * @param string $artNo Article number.
     *
     * @link http://integration.klarna.com/en/api/invoice-handling-functions/
     *       functions/mkartno
     *
     * @throws KlarnaException
     * @return void
     */
    public function addArtNo($qty, $artNo)
    {
        $this->_checkQty($qty);
        $this->_checkArtNo($artNo);

        if (!is_array($this->artNos)) {
            $this->artNos = array();
        }

        $this->artNos[] = array('artno' => $artNo, 'qty' => $qty);
    }

    /**
     * Partially activates a passive invoice.
     *
     * Returned array contains index "url" and "invno".<br>
     * The value of "url" is a URL pointing to a temporary PDF-version of the
     * activated invoice.<br>
     * The value of "invno" is either 0 if the entire invoice was activated or
     * the number on the new passive invoice.<br>
     *
     * <b>Note</b>:<br>
     * You need to call {@link Klarna::addArtNo()} first, to specify which
     * articles and how many you want to partially activate.<br>
     * If you want to change the shipment type, you can specify it using:
     * {@link Klarna::setShipmentInfo() setShipmentInfo('delay_adjust', ...)}
     * with either: {@link KlarnaFlags::NORMAL_SHIPMENT NORMAL_SHIPMENT}
     * or {@link KlarnaFlags::EXPRESS_SHIPMENT EXPRESS_SHIPMENT}
     *
     * @param string $invNo  Invoice numbers.
     * @param int    $pclass PClass id used for this invoice.
     * @param bool   $clear  Whether customer info should be cleared after
     *                       this call.
     *
     * @see Klarna::addArtNo()
     * @see Klarna::activateInvoice()
     * @link http://integration.klarna.com/en/api/standard-integration/functions
     *       /activatepart
     *
     * @throws KlarnaException
     * @return array An array with invoice URL and invoice number.
     *         ['url' => val, 'invno' => val]
     */
    public function activatePart(
        $invNo, $pclass = KlarnaPClass::INVOICE, $clear = true
    ) {
        $this->_checkInvNo($invNo);
        $this->_checkArtNos($this->artNos);

        self::printDebug('activate_part artNos array', $this->artNos);

        //function activate_part_digest
        $string = $this->_eid . ":" . $invNo . ":";
        foreach ($this->artNos as $artNo) {
            $string .= $artNo["artno"] . ":". $artNo["qty"] . ":";
        }
        $digestSecret = self::digest($string . $this->_secret);
        //end activate_part_digest

        $paramList = array(
            $this->_eid,
            $invNo,
            $this->artNos,
            $digestSecret,
            $pclass,
            $this->shipInfo
        );

        self::printDebug('activate_part array', $paramList);

        $result = $this->xmlrpc_call('activate_part', $paramList);

        if ($clear === true) {
            $this->clear();
        }

        self::printDebug('activate_part result', $result);

        return $result;
    }

    /**
     * Retrieves the total amount for an active invoice.
     *
     * @param string $invNo Invoice number.
     *
     * @link http://integration.klarna.com/en/api/other-functions/functions
     *       /invoiceamount
     *
     * @throws KlarnaException
     * @return float The total amount.
     */
    public function invoiceAmount($invNo)
    {
        $this->_checkInvNo($invNo);

        $digestSecret = self::digest(
            $this->colon($this->_eid, $invNo, $this->_secret)
        );

        $paramList = array(
            $this->_eid,
            $invNo,
            $digestSecret
        );

        self::printDebug('invoice_amount array', $paramList);

        $result = $this->xmlrpc_call('invoice_amount', $paramList);

        //Result is in cents, fix it.
        return ($result / 100);
    }

    /**
     * Changes the order number of a purchase that was set when the order was
     * made online.
     *
     * @param string $invNo   Invoice number.
     * @param string $orderid Estores order number.
     *
     * @link http://integration.klarna.com/en/api/other-functions/functions
     *       /updateorderno
     *
     * @throws KlarnaException
     * @return string  Invoice number.
     */
    public function updateOrderNo($invNo, $orderid)
    {
        $this->_checkInvNo($invNo);
        $this->_checkEstoreOrderNo($orderid);

        $digestSecret = self::digest(
            $this->colon($invNo, $orderid, $this->_secret)
        );

        $paramList = array(
            $this->_eid,
            $digestSecret,
            $invNo,
            $orderid
        );

        self::printDebug('update_orderno array', $paramList);

        $result = $this->xmlrpc_call('update_orderno', $paramList);

        return $result;
    }

    /**
     * Sends an activated invoice to the customer via e-mail. <br>
     * The email is sent in plain text format and contains a link to a
     * PDF-invoice.<br>
     *
     * <b>Please note!</b><br>
     * Regular postal service is used if the customer has not entered his/her
     * e-mail address when making the purchase (charges may apply).<br>
     *
     * @param string $invNo Invoice number.
     *
     * @link http://integration.klarna.com/en/api/invoice-handling-functions
     *       /functions/emailinvoice
     *
     * @throws KlarnaException
     * @return string  Invoice number.
     */
    public function emailInvoice($invNo)
    {
        $this->_checkInvNo($invNo);

        $digestSecret = self::digest(
            $this->colon($this->_eid, $invNo, $this->_secret)
        );
        $paramList = array(
            $this->_eid,
            $invNo,
            $digestSecret
        );

        self::printDebug('email_invoice array', $paramList);

        return $this->xmlrpc_call('email_invoice', $paramList);
    }

    /**
     * Requests a postal send-out of an activated invoice to a customer by
     * Klarna (charges may apply).
     *
     * @param string $invNo Invoice number.
     *
     * @link http://integration.klarna.com/en/api/invoice-handling-functions
     *       /functions/sendinvoice
     *
     * @throws KlarnaException
     * @return string  Invoice number.
     */
    public function sendInvoice($invNo)
    {
        $this->_checkInvNo($invNo);

        $digestSecret = self::digest(
            $this->colon($this->_eid, $invNo, $this->_secret)
        );
        $paramList = array(
            $this->_eid,
            $invNo,
            $digestSecret
        );

        self::printDebug('send_invoice array', $paramList);

        return $this->xmlrpc_call('send_invoice', $paramList);
    }

    /**
     * Gives discounts on invoices.<br>
     * If you are using standard integration and the purchase is not yet
     * activated (you have not yet delivered the goods), <br>
     * just change the article list in our online interface Klarna Online.<br>
     *
     * <b>Flags can be</b>:<br>
     * {@link KlarnaFlags::INC_VAT}<br>
     * {@link KlarnaFlags::NO_FLAG}, <b>NOT RECOMMENDED!</b><br>
     *
     * @param string $invNo       Invoice number.
     * @param int    $amount      The amount given as a discount.
     * @param float  $vat         VAT in percent, e.g. 22.2 for 22.2%.
     * @param int    $flags       If amount is
     *                            {@link KlarnaFlags::INC_VAT including} or
     *                            {@link KlarnaFlags::NO_FLAG excluding} VAT.
     * @param string $description Optional custom text to present as discount
     *                            in the invoice.
     *
     * @link http://integration.klarna.com/en/api/invoice-handling-functions
     *       /functions/returnamount
     *
     * @throws KlarnaException
     * @return string  Invoice number.
     */
    public function returnAmount(
        $invNo, $amount, $vat, $flags = KlarnaFlags::INC_VAT, $description = ""
    ) {
        $this->_checkInvNo($invNo);
        $this->_checkAmount($amount);
        $this->_checkVAT($vat);
        $this->_checkInt($flags, 'flags');

        if ($description == null) {
            $description = "";
        }

        $digestSecret = self::digest(
            $this->colon($this->_eid, $invNo, $this->_secret)
        );
        $paramList = array(
            $this->_eid,
            $invNo,
            $amount,
            $vat,
            $digestSecret,
            $flags,
            $description
        );

        self::printDebug('return_amount', $paramList);

        return $this->xmlrpc_call('return_amount', $paramList);
    }

    /**
     * Performs a complete refund on an invoice, part payment and mobile
     * purchase.
     *
     * @param string $invNo  Invoice number.
     * @param string $credNo Credit number.
     *
     * @link http://integration.klarna.com/en/api/invoice-handling-functions
     *       /functions/creditinvoice
     *
     * @throws KlarnaException
     * @return string  Invoice number.
     */
    public function creditInvoice($invNo, $credNo = "")
    {
        $this->_checkInvNo($invNo);
        $this->_checkCredNo($credNo);

        $digestSecret = self::digest(
            $this->colon($this->_eid, $invNo, $this->_secret)
        );
        $paramList = array(
            $this->_eid,
            $invNo,
            $credNo,
            $digestSecret
        );

        self::printDebug('credit_invoice', $paramList);

        return $this->xmlrpc_call('credit_invoice', $paramList);
    }

    /**
     * Performs a partial refund on an invoice, part payment or mobile purchase.
     *
     * <b>Note</b>:<br>
     * You need to call {@link Klarna::addArtNo()} first.<br>
     *
     * @param string $invNo  Invoice number.
     * @param string $credNo Credit number.
     *
     * @see  Klarna::addArtNo()
     * @link http://integration.klarna.com/en/api/invoice-handling-functions
     *       /functions/creditpart
     *
     * @throws KlarnaException
     * @return string  Invoice number.
     */
    public function creditPart($invNo, $credNo = "")
    {
        $this->_checkInvNo($invNo);
        $this->_checkCredNo($credNo);

        if ($this->goodsList === null || empty($this->goodsList)) {
            $this->_checkArtNos($this->artNos);
        }

        //function activate_part_digest
        $string = $this->_eid . ":" . $invNo . ":";

        if ($this->artNos !== null && !empty($this->artNos)) {
            foreach ($this->artNos as $artNo) {
                $string .= $artNo["artno"] . ":". $artNo["qty"] . ":";
            }
        }

        $digestSecret = self::digest($string . $this->_secret);
        //end activate_part_digest

        $paramList = array(
            $this->_eid,
            $invNo,
            $this->artNos,
            $credNo,
            $digestSecret
        );

        if ($this->goodsList !== null && !empty($this->goodsList)) {
            $paramList[] = 0;
            $paramList[] = $this->goodsList;
        }

        $this->artNos = array();

        self::printDebug('credit_part', $paramList);

        return $this->xmlrpc_call('credit_part', $paramList);
    }

    /**
     * Changes the quantity of a specific item in a passive invoice.
     *
     * @param string $invNo Invoice number.
     * @param string $artNo Article number.
     * @param int    $qty   Quantity of specified article.
     *
     * @link http://integration.klarna.com/en/api/other-functions/functions
     *       /updategoodsqty
     *
     * @throws KlarnaException
     * @return string  Invoice number.
     */
    public function updateGoodsQty($invNo, $artNo, $qty)
    {
        $this->_checkInvNo($invNo);
        $this->_checkQty($qty);
        $this->_checkArtNo($artNo);

        $digestSecret = self::digest(
            $this->colon($invNo, $artNo, $qty, $this->_secret)
        );

        $paramList = array(
            $this->_eid,
            $digestSecret,
            $invNo,
            $artNo,
            $qty
        );

        self::printDebug('update_goods_qty', $paramList);

        return $this->xmlrpc_call('update_goods_qty', $paramList);
    }

    /**
     * Changes the amount of a fee (e.g. the invoice fee) in a passive invoice.
     *
     * <b>Type can be</b>:<br>
     * {@link KlarnaFlags::IS_SHIPMENT}<br>
     * {@link KlarnaFlags::IS_HANDLING}<br>
     *
     * @param string $invNo     Invoice number.
     * @param int    $type      Charge type.
     * @param int    $newAmount The new amount for the charge.
     *
     * @link http://integration.klarna.com/en/api/other-functions/functions
     *       /updatechargeamount
     *
     * @throws KlarnaException
     * @return string  Invoice number.
     */
    public function updateChargeAmount($invNo, $type, $newAmount)
    {
        $this->_checkInvNo($invNo);
        $this->_checkInt($type, 'type');
        $this->_checkAmount($newAmount);

        if ($type === KlarnaFlags::IS_SHIPMENT) {
            $type = 1;
        } else if ($type === KlarnaFlags::IS_HANDLING) {
            $type = 2;
        }

        $digestSecret = self::digest(
            $this->colon($invNo, $type, $newAmount, $this->_secret)
        );

        $paramList = array(
            $this->_eid,
            $digestSecret,
            $invNo,
            $type,
            $newAmount
        );

        self::printDebug('update_charge_amount', $paramList);

        return $this->xmlrpc_call('update_charge_amount', $paramList);
    }

    /**
     * The invoice_address function is used to retrieve the address of a
     * purchase.
     *
     * @param string $invNo Invoice number.
     *
     * @link http://integration.klarna.com/en/api/other-functions/functions
     *       /invoiceaddress
     *
     * @throws KlarnaException
     * @return KlarnaAddr
     */
    public function invoiceAddress($invNo)
    {
        $this->_checkInvNo($invNo);

        $digestSecret = self::digest(
            $this->colon($this->_eid, $invNo, $this->_secret)
        );
        $paramList = array(
            $this->_eid,
            $invNo,
            $digestSecret
        );

        self::printDebug('invoice_address', $paramList);

        $result = $this->xmlrpc_call('invoice_address', $paramList);

        $addr = new KlarnaAddr();
        if (strlen($result[0]) > 0) {
            $addr->isCompany = false;
            $addr->setFirstName($result[0]);
            $addr->setLastName($result[1]);
        } else {
            $addr->isCompany = true;
            $addr->setCompanyName($result[1]);
        }
        $addr->setStreet($result[2]);
        $addr->setZipCode($result[3]);
        $addr->setCity($result[4]);
        $addr->setCountry($result[5]);

        return $addr;
    }

    /**
     * Retrieves the amount of a specific goods from a purchase.
     *
     * <b>Note</b>:<br>
     * You need to call {@link Klarna::addArtNo()} first.<br>
     *
     * @param string $invNo Invoice number.
     *
     * @link http://integration.klarna.com/en/api/other-functions/functions
     *       /invoicepartamount
     * @see  Klarna::addArtNo()
     *
     * @throws KlarnaException
     * @return float The amount of the goods.
     */
    public function invoicePartAmount($invNo)
    {
        $this->_checkInvNo($invNo);
        $this->_checkArtNos($this->artNos);

        //function activate_part_digest
        $string = $this->_eid . ":" . $invNo . ":";
        foreach ($this->artNos as $artNo) {
            $string .= $artNo["artno"] . ":". $artNo["qty"] . ":";
        }
        $digestSecret = self::digest($string . $this->_secret);
        //end activate_part_digest

        $paramList = array(
            $this->_eid,
            $invNo,
            $this->artNos,
            $digestSecret
        );
        $this->artNos = array();

        self::printDebug('invoice_part_amount', $paramList);

        $result = $this->xmlrpc_call('invoice_part_amount', $paramList);

        return ($result / 100);
    }

    /**
     * Returns the current order status for a specific reservation or invoice.
     * Use this when {@link Klarna::addTransaction()} or
     * {@link Klarna::reserveAmount()} returns a {@link KlarnaFlags::PENDING}
     * status.
     *
     * <b>Order status can be</b>:<br>
     * {@link KlarnaFlags::ACCEPTED}<br>
     * {@link KlarnaFlags::PENDING}<br>
     * {@link KlarnaFlags::DENIED}<br>
     *
     * @param string $id   Reservation number or invoice number.
     * @param int    $type 0 if $id is an invoice or reservation, 1 for order id
     *
     * @link http://integration.klarna.com/en/api/other-functions/functions
     *       /checkorderstatus
     *
     * @throws KlarnaException
     * @return string  The order status.
     */
    public function checkOrderStatus($id, $type = 0)
    {
        $this->_checkArgument($id, "id");

        $this->_checkInt($type, 'type');
        if ($type !== 0 && $type !== 1) {
            throw new Klarna_InvalidTypeException(
                'type', "0 or 1"
            );
        }

        $digestSecret = self::digest(
            $this->colon($this->_eid, $id, $this->_secret)
        );
        $paramList = array(
            $this->_eid,
            $digestSecret,
            $id,
            $type
        );

        self::printDebug('check_order_status', $paramList);

        return $this->xmlrpc_call('check_order_status', $paramList);
    }

    /**
     * Retrieves a list of all the customer numbers associated with the
     * specified pno.
     *
     * @param string $pno      Social security number, Personal number, ...
     * @param int    $encoding {@link KlarnaEncoding PNO Encoding} constant.
     *
     * @throws KlarnaException
     * @return array An array containing all customer numbers associated
     *               with that pno.
     */
    public function getCustomerNo($pno, $encoding = null)
    {
        //Get the PNO/SSN encoding constant.
        if ($encoding === null) {
            $encoding = $this->getPNOEncoding();
        }
        $this->_checkPNO($pno, $encoding);

        $digestSecret = self::digest(
            $this->colon($this->_eid, $pno, $this->_secret)
        );
        $paramList = array(
            $pno,
            $this->_eid,
            $digestSecret,
            $encoding
        );

        self::printDebug('get_customer_no', $paramList);

        return $this->xmlrpc_call('get_customer_no', $paramList);
    }

    /**
     * Associates a pno with a customer number when you want to make future
     * purchases without a pno.
     *
     * @param string $pno      Social security number, Personal number, ...
     * @param string $custNo   The customer number.
     * @param int    $encoding {@link KlarnaEncoding PNO Encoding} constant.
     *
     * @throws KlarnaException
     * @return bool  True, if the customer number was associated with the pno.
     */
    public function setCustomerNo($pno, $custNo, $encoding = null)
    {
        //Get the PNO/SSN encoding constant.
        if ($encoding === null) {
            $encoding = $this->getPNOEncoding();
        }
        $this->_checkPNO($pno, $encoding);

        $this->_checkArgument($custNo, 'custNo');

        $digestSecret = self::digest(
            $this->colon($this->_eid, $pno, $custNo, $this->_secret)
        );
        $paramList = array(
            $pno,
            $custNo,
            $this->_eid,
            $digestSecret,
            $encoding
        );

        self::printDebug('set_customer_no', $paramList);

        $result = $this->xmlrpc_call('set_customer_no', $paramList);

        return ($result == 'ok');
    }

    /**
     * Removes a customer number from association with a pno.
     *
     * @param string $custNo The customer number.
     *
     * @throws KlarnaException
     * @return bool    True, if the customer number association was removed.
     */
    public function removeCustomerNo($custNo)
    {
        $this->_checkArgument($custNo, 'custNo');

        $digestSecret = self::digest(
            $this->colon($this->_eid, $custNo, $this->_secret)
        );

        $paramList = array(
            $custNo,
            $this->_eid,
            $digestSecret
        );

        self::printDebug('remove_customer_no', $paramList);

        $result = $this->xmlrpc_call('remove_customer_no', $paramList);

        return ($result == 'ok');
    }

    /**
     * Sets notes/log information for the specified invoice  number.
     *
     * @param string $invNo Invoice number.
     * @param string $notes Note(s) to be associated with the invoice.
     *
     * @throws KlarnaException
     * @return string  Invoice number.
     */
    public function updateNotes($invNo, $notes)
    {
        $this->_checkInvNo($invNo);

        if (!is_string($notes)) {
            $notes = strval($notes);
        }

        $digestSecret = self::digest(
            $this->colon($invNo, $notes, $this->_secret)
        );

        $paramList = array(
            $this->_eid,
            $digestSecret,
            $invNo,
            $notes
        );

        self::printDebug('update_notes', $paramList);

        return $this->xmlrpc_call('update_notes', $paramList);
    }

    /**
     * Returns the configured PCStorage object.
     *
     * @throws Exception|KlarnaException
     * @return PCStorage
     */
    public function getPCStorage()
    {
        if (isset($this->pclasses)) {
            return $this->pclasses;
        }

        include_once 'pclasses/storage.intf.php';
        $className = $this->pcStorage.'storage';
        $pclassStorage = dirname(__FILE__) . "/pclasses/{$className}.class.php";

        include_once $pclassStorage;
        $storage = new $className;

        if (!($storage instanceof PCStorage)) {
            throw new Klarna_PCStorageInvalidException(
                $className, $pclassStorage
            );
        }
        return $storage;
    }

    /**
     * Fetch pclasses
     *
     * @param PCStorage $storage  PClass Storage
     * @param int       $country  KlarnaCountry constant
     * @param int       $language KlarnaLanguage constant
     * @param int       $currency KlarnaCurrency constant
     *
     * @return void
     */
    private function _fetchPClasses($storage, $country, $language, $currency)
    {
        $digestSecret = self::digest(
            $this->_eid . ":" . $currency . ":" . $this->_secret
        );
        $paramList = array(
            $this->_eid,
            $currency,
            $digestSecret,
            $country,
            $language
        );

        self::printDebug('get_pclasses array', $paramList);

        $result = $this->xmlrpc_call('get_pclasses', $paramList);

        self::printDebug('get_pclasses result', $result);

        foreach ($result as &$pclass) {
            //numeric htmlentities
            $pclass[1] = Klarna::num_htmlentities($pclass[1]);

            //Below values are in "cents", fix them.
            $pclass[3] /= 100; //divide start fee with 100
            $pclass[4] /= 100; //divide invoice fee with 100
            $pclass[5] /= 100; //divide interest rate with 100
            $pclass[6] /= 100; //divide min amount with 100

            if ($pclass[9] != '-') {
                //unix timestamp instead of yyyy-mm-dd
                $pclass[9] = strtotime($pclass[9]);
            }

            //Associate the PClass with this estore.
            array_unshift($pclass, $this->_eid);

            $storage->addPClass(new KlarnaPClass($pclass));
        }
    }

    /**
     * Fetches the PClasses from Klarna Online.<br>
     * Removes the cached/stored pclasses and updates.<br>
     * You are only allowed to call this once, or once per update of PClasses
     * in KO.<br>
     *
     * <b>Note</b>:<br>
     * If language and/or currency is null, then they will be set to mirror
     * the specified country.<br/>
     * Short codes like DE, SV or EUR can also be used instead of the constants.
     *
     * @param string|int $country  {@link KlarnaCountry Country} constant,
     *                             or two letter code.
     * @param mixed      $language {@link KlarnaLanguage Language} constant,
     *                             or two letter code.
     * @param mixed      $currency {@link KlarnaCurrency Currency} constant,
     *                             or three letter code.
     *
     * @throws KlarnaException
     * @return void
     */
    public function fetchPClasses(
        $country = null, $language = null, $currency = null
    ) {
        extract(
            $this->getLocale($country, $language, $currency),
            EXTR_OVERWRITE
        );

        $this->_checkConfig();

        $pclasses = $this->getPCStorage();
        try {
            //Attempt to load previously stored pclasses, so they aren't
            // accidentially removed.
            $pclasses->load($this->pcURI);
        }
        catch(Exception $e) {
            self::printDebug('load pclasses', $e->getMessage());
        }

        $this->_fetchPClasses($pclasses, $country, $language, $currency);

        $pclasses->save($this->pcURI);
        $this->pclasses = $pclasses;
    }

    /**
     * Removes the stored PClasses, if you need to update them.
     *
     * @throws KlarnaException
     * @return void
     */
    public function clearPClasses()
    {
        $this->_checkConfig();

        $pclasses = $this->getPCStorage();
        $pclasses->clear($this->pcURI);
    }

    /**
     * Retrieves the specified PClasses.
     *
     * <b>Type can be</b>:<br>
     * {@link KlarnaPClass::CAMPAIGN}<br>
     * {@link KlarnaPClass::ACCOUNT}<br>
     * {@link KlarnaPClass::SPECIAL}<br>
     * {@link KlarnaPClass::FIXED}<br>
     * {@link KlarnaPClass::DELAY}<br>
     * {@link KlarnaPClass::MOBILE}<br>
     *
     * @param int $type PClass type identifier.
     *
     * @throws KlarnaException
     * @return array An array of PClasses. [KlarnaPClass]
     */
    public function getPClasses($type = null)
    {
        $this->_checkConfig();

        if (!$this->pclasses) {
            $this->pclasses = $this->getPCStorage();
            $this->pclasses->load($this->pcURI);
        }
        $tmp = $this->pclasses->getPClasses(
            $this->_eid, $this->_country, $type
        );
        $this->sortPClasses($tmp[$this->_eid]);
        return $tmp[$this->_eid];
    }

    /**
     * Retrieve a flattened array of all pclasses stored in the configured
     * pclass storage.
     *
     * @return array
     */
    public function getAllPClasses()
    {
        if (!$this->pclasses) {
            $this->pclasses = $this->getPCStorage();
            $this->pclasses->load($this->pcURI);
        }
        return $this->pclasses->getAllPClasses();
    }

    /**
     * Returns the specified PClass.
     *
     * @param int $id The PClass ID.
     *
     * @return KlarnaPClass
     */
    public function getPClass($id)
    {
        if (!is_numeric($id)) {
            throw new Klarna_InvalidTypeException('id', 'integer');
        }

        $this->_checkConfig();

        if (!$this->pclasses || !($this->pclasses instanceof PCStorage)) {
            $this->pclasses = $this->getPCStorage();
            $this->pclasses->load($this->pcURI);
        }
        return $this->pclasses->getPClass(
            intval($id), $this->_eid, $this->_country
        );
    }

    /**
     * Sorts the specified array of KlarnaPClasses.
     *
     * @param array &$array An array of {@link KlarnaPClass PClasses}.
     *
     * @return void
     */
    public function sortPClasses(&$array)
    {
        if (!is_array($array)) {
            //Input is not an array!
            $array = array();
            return;
        }
        //Sort pclasses array after natural sort (natcmp)
        if (!function_exists('pcCmp')) {
            /**
             * Comparison function
             *
             * @param KlarnaPClass $a object 1
             * @param KlarnaPClass $b object 2
             *
             * @return int
             */
            function pcCmp($a, $b)
            {
                if ($a->getDescription() == null
                    && $b->getDescription() == null
                ) {
                    return 0;
                } else if ($a->getDescription() == null) {
                    return 1;
                } else if ($b->getDescription() == null) {
                    return -1;
                } else if ($b->getType() === 2 && $a->getType() !== 2) {
                    return 1;
                } else if ($b->getType() !== 2 && $a->getType() === 2) {
                    return -1;
                }

                return strnatcmp($a->getDescription(), $b->getDescription())*-1;
            }
        }
        usort($array, "pcCmp");
    }

    /**
     * Returns the cheapest, per month, PClass related to the specified sum.
     *
     * <b>Note</b>: This choose the cheapest PClass for the current country.<br>
     * {@link Klarna::setCountry()}
     *
     * <b>Flags can be</b>:<br>
     * {@link KlarnaFlags::CHECKOUT_PAGE}<br>
     * {@link KlarnaFlags::PRODUCT_PAGE}<br>
     *
     * @param float $sum   The product cost, or total sum of the cart.
     * @param int   $flags Which type of page the info will be displayed on.
     *
     * @throws KlarnaException
     * @return KlarnaPClass or false if none was found.
     */
    public function getCheapestPClass($sum, $flags)
    {
        if (!is_numeric($sum)) {
            throw new Klarna_InvalidPriceException($sum);
        }

        if (!is_numeric($flags)
            || !in_array(
                $flags, array(
                    KlarnaFlags::CHECKOUT_PAGE, KlarnaFlags::PRODUCT_PAGE)
            )
        ) {
            throw new Klarna_InvalidTypeException(
                'flags',
                KlarnaFlags::CHECKOUT_PAGE . ' or ' . KlarnaFlags::PRODUCT_PAGE
            );
        }

        $lowest_pp = $lowest = false;

        foreach ($this->getPClasses() as $pclass) {
            $lowest_payment = KlarnaCalc::get_lowest_payment_for_account(
                $pclass->getCountry()
            );
            if ($pclass->getType() < 2 && $sum >= $pclass->getMinAmount()) {
                $minpay = KlarnaCalc::calc_monthly_cost(
                    $sum, $pclass, $flags
                );

                if ($minpay < $lowest_pp || $lowest_pp === false) {
                    if ($pclass->getType() == KlarnaPClass::ACCOUNT
                        || $minpay >= $lowest_payment
                    ) {
                        $lowest_pp = $minpay;
                        $lowest = $pclass;
                    }
                }
            }
        }

        return $lowest;
    }

    /**
     * Initializes the checkoutHTML objects.
     *
     * @see Klarna::checkoutHTML()
     * @return void
     */
    protected function initCheckout()
    {
        $dir = dirname(__FILE__);

        //Require the CheckoutHTML interface/abstract class
        include_once $dir.'/checkout/checkouthtml.intf.php';

        //Iterate over all .class.php files in checkout/
        foreach (glob($dir.'/checkout/*.class.php') as $checkout) {
            if (!self::$debug) {
                ob_start();
            }
            include_once $checkout;

            $className = basename($checkout, '.class.php');
            $cObj = new $className;

            if ($cObj instanceof CheckoutHTML) {
                $cObj->init($this, $this->_eid);
                $this->coObjects[$className] = $cObj;
            }

            if (!self::$debug) {
                ob_end_clean();
            }
        }
    }

    /**
     * Returns the checkout page HTML from the checkout classes.
     *
     * <b>Note</b>:<br>
     * This method uses output buffering to silence unwanted echoes.<br>
     *
     * @see CheckoutHTML
     *
     * @return string  A HTML string.
     */
    public function checkoutHTML()
    {
        if (empty($this->coObjects)) {
            $this->initCheckout();
        }
        $dir = dirname(__FILE__);

        //Require the CheckoutHTML interface/abstract class
        include_once $dir.'/checkout/checkouthtml.intf.php';

        //Iterate over all .class.php files in
        $html = "\n";
        foreach ($this->coObjects as $cObj) {
            if (!self::$debug) {
                ob_start();
            }
            if ($cObj instanceof CheckoutHTML) {
                $html .= $cObj->toHTML() . "\n";
            }
            if (!self::$debug) {
                ob_end_clean();
            }
        }

        return $html;
    }

    /**
     * Creates a XMLRPC call with specified XMLRPC method and parameters from array.
     *
     * @param string $method XMLRPC method.
     * @param array  $array  XMLRPC parameters.
     *
     * @throws KlarnaException
     * @return mixed
     */
    protected function xmlrpc_call($method, $array)
    {
        $this->_checkConfig();

        if (!isset($method) || !is_string($method)) {
            throw new Klarna_InvalidTypeException('method', 'string');
        }
        if ($array === null || count($array) === 0) {
            throw new KlarnaException("Parameterlist is empty or null!", 50067);
        }
        if (self::$disableXMLRPC) {
            return true;
        }
        try {
            /*
             * Disable verifypeer for CURL, so below error is avoided.
             * CURL error: SSL certificate problem, verify that the CA
             * cert is OK.
             * Details: error:14090086:SSL
             * routines:SSL3_GET_SERVER_CERTIFICATE:certificate verify failed (#8)
             */
            $this->xmlrpc->verifypeer = false;

            $timestart = microtime(true);

            //Create the XMLRPC message.
            $msg = new xmlrpcmsg($method);
            $params = array_merge(
                array(
                    $this->PROTO, $this->VERSION
                ), $array
            );

            $msg = new xmlrpcmsg($method);
            foreach ($params as $p) {
                if (!$msg->addParam(
                    php_xmlrpc_encode($p, array('extension_api'))
                )
                ) {
                    throw new KlarnaException(
                        "Failed to add parameters to XMLRPC message.",
                        50068
                    );
                }
            }

            //Send the message.
            $selectDateTime = microtime(true);
            if (self::$xmlrpcDebug) {
                $this->xmlrpc->setDebug(2);
            }
            $xmlrpcresp = $this->xmlrpc->send($msg);

            //Calculate time and selectTime.
            $timeend = microtime(true);
            $time = (int) (($selectDateTime - $timestart) * 1000);
            $selectTime = (int) (($timeend - $timestart) * 1000);

            $status = $xmlrpcresp->faultCode();

            //Send report to candice.
            if (self::$_candice === true) {
                $this->sendStat($method, $time, $selectTime, $status);
            }

            if ($status !== 0) {
                throw new KlarnaException($xmlrpcresp->faultString(), $status);
            }

            return php_xmlrpc_decode($xmlrpcresp->value());
        }
        catch(KlarnaException $e) {
            //Otherwise it is caught below, and rethrown.
            throw $e;
        }
        catch(Exception $e) {
            throw new KlarnaException($e->getMessage(), $e->getCode());
        }
    }

    /**
     * Removes all relevant order/customer data from the internal structure.
     *
     * @return void
     */
    public function clear()
    {
        $this->goodsList = null;
        $this->comment = "";

        $this->billing = null;
        $this->shipping = null;

        $this->shipInfo = array();
        $this->extraInfo = array();
        $this->bankInfo = array();
        $this->incomeInfo = array();
        $this->activateInfo = array();

        $this->reference = "";
        $this->reference_code = "";

        $this->orderid[0] = "";
        $this->orderid[1] = "";

        $this->artNos = array();
        $this->coObjects = array();
    }

    /**
     * Sends a report to Candice.
     *
     * @param string $method     XMLRPC method.
     * @param int    $time       Elapsed time of entire XMLRPC call.
     * @param int    $selectTime Time to create the XMLRPC parameters.
     * @param int    $status     XMLRPC error code.
     *
     * @return void
     */
    protected function sendStat($method, $time, $selectTime, $status)
    {
        $fp = @fsockopen('udp://'.self::$_c_addr, 80, $errno, $errstr, 1500);
        if ($fp) {
            $uri = "{$this->_url['scheme']}://{$this->_url['host']}" .
                    ":{$this->_url['port']}";

            $data = $this->pipe(
                $this->_eid,
                $method,
                $time,
                $selectTime,
                $status,
                $uri
            );
            $digest = self::digest($this->pipe($data, $this->_secret));

            self::printDebug("candice report", $data);

            @fwrite($fp, $this->pipe($data, $digest));
            @fclose($fp);
        }
    }

    /**
     * Implodes parameters with delimiter ':'.
     * Null and "" values are ignored by the colon function to
     * ensure there is not several colons in succession.
     *
     * @return string Colon separated string.
     */
    public static function colon(/* variable parameters */)
    {
        return implode(
            ':',
            array_filter(
                func_get_args(),
                array('self', 'filterDigest')
            )
        );
    }

    /**
     * Implodes parameters with delimiter '|'.
     *
     * @return string Pipe separated string.
     */
    public static function pipe(/* variable parameters */)
    {
        $args = func_get_args();
        return implode('|', $args);
    }

    /**
     * Check if the value has a string length larger than 0
     *
     * @param mixed $value The value to check.
     *
     * @return boolean True if string length is larger than 0
     */
    public static function filterDigest($value)
    {
        return strlen(strval($value)) > 0;
    }

    /**
     * Creates a digest hash from the inputted string,
     * and the specified or the preferred hash algorithm.
     *
     * @param string $data Data to be hashed.
     * @param string $hash hash algoritm to use
     *
     * @throws KlarnaException
     * @return string  Base64 encoded hash.
     */
    public static function digest($data, $hash = null)
    {
        if ($hash===null) {
            $preferred = array(
                'sha512',
                'sha384',
                'sha256',
                'sha224',
                'md5'
            );

            $hashes = array_intersect($preferred, hash_algos());

            if (count($hashes) == 0) {
                throw new KlarnaException(
                    "No available hash algorithm supported!"
                );
            }
            $hash = array_shift($hashes);
        }
        self::printDebug('digest() using hash', $hash);

        return base64_encode(pack("H*", hash($hash, $data)));
    }

    /**
     * Converts special characters to numeric htmlentities.
     *
     * <b>Note</b>:<br>
     * If supplied string is encoded with UTF-8, o umlaut ("ö") will become two
     * HTML entities instead of one.
     *
     * @param string $str String to be converted.
     *
     * @return string String converted to numeric HTML entities.
     */
    public static function num_htmlentities($str)
    {
        if (!self::$htmlentities) {
            self::$htmlentities = array();
            $table = get_html_translation_table(HTML_ENTITIES, ENT_QUOTES);
            foreach ($table as $char => $entity) {
                self::$htmlentities[$entity] = '&#' . ord($char) . ';';
            }
        }

        return str_replace(
            array_keys(
                self::$htmlentities
            ), self::$htmlentities, htmlentities($str)
        );
    }

    /**
     * Prints debug information if debug is set to true.
     * $msg is used as header/footer in the output.
     *
     * if FirePHP is available it will be used instead of
     * dumping the debug info into the document.
     *
     * It uses print_r and encapsulates it in HTML/XML comments.
     * (<!-- -->)
     *
     * @param string $msg   Debug identifier, e.g. "my array".
     * @param mixed  $mixed Object, type, etc, to be debugged.
     *
     * @return void
     */
    public static function printDebug($msg, $mixed)
    {
        if (self::$debug) {
            if (class_exists('FB', false)) {
                FB::send($mixed, $msg);
            } else {
                echo "\n<!-- ".$msg.": \n";
                print_r($mixed);
                echo "\n end ".$msg." -->\n";
            }
        }
    }

    /**
     * Checks/fixes so the invNo input is valid.
     *
     * @param string &$invNo Invoice number.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkInvNo(&$invNo)
    {
        if (!isset($invNo)) {
            throw new Klarna_ArgumentNotSetException("Invoice number");
        }
        if (!is_string($invNo)) {
            $invNo = strval($invNo);
        }
        if (strlen($invNo) == 0) {
            throw new Klarna_ArgumentNotSetException("Invoice number");
        }
    }

    /**
     * Checks/fixes so the quantity input is valid.
     *
     * @param int &$qty Quantity.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkQty(&$qty)
    {
        if (!isset($qty)) {
            throw new Klarna_ArgumentNotSetException("Quantity");
        }
        if (is_numeric($qty) && !is_int($qty)) {
            $qty = intval($qty);
        }
        if (!is_int($qty)) {
            throw new Klarna_InvalidTypeException("Quantity", "integer");
        }
    }

    /**
     * Checks/fixes so the artTitle input is valid.
     *
     * @param string &$artTitle Article title.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkArtTitle(&$artTitle)
    {
        if (!is_string($artTitle)) {
            $artTitle = strval($artTitle);
        }
        if (!isset($artTitle) || strlen($artTitle) == 0) {
            throw new Klarna_ArgumentNotSetException("artTitle", 50059);
        }
    }

    /**
     * Checks/fixes so the artNo input is valid.
     *
     * @param int|string &$artNo Article number.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkArtNo(&$artNo)
    {
        if (is_numeric($artNo) && !is_string($artNo)) {
            //Convert artNo to string if integer.
            $artNo = strval($artNo);
        }
        if (!isset($artNo) || strlen($artNo) == 0 || (!is_string($artNo))) {
            throw new Klarna_ArgumentNotSetException("artNo");
        }
    }

    /**
     * Checks/fixes so the credNo input is valid.
     *
     * @param string &$credNo Credit number.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkCredNo(&$credNo)
    {
        if (!isset($credNo)) {
            throw new Klarna_ArgumentNotSetException("Credit number");
        }

        if ($credNo === false || $credNo === null) {
            $credNo = "";
        }
        if (!is_string($credNo)) {
            $credNo = strval($credNo);
            if (!is_string($credNo)) {
                throw new Klarna_InvalidTypeException("Credit number", "string");
            }
        }
    }

    /**
     * Checks so that artNos is an array and is not empty.
     *
     * @param array &$artNos Array from {@link Klarna::addArtNo()}.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkArtNos(&$artNos)
    {
        if (!is_array($artNos)) {
            throw new Klarna_InvalidTypeException("artNos", "array");
        }
        if (empty($artNos)) {
            throw new KlarnaException('ArtNo array is empty!', 50064);
        }
    }

    /**
     * Checks/fixes so the integer input is valid.
     *
     * @param int    &$int  {@link KlarnaFlags flags} constant.
     * @param string $field Name of the field.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkInt(&$int, $field)
    {
        if (!isset($int)) {
            throw new Klarna_ArgumentNotSetException($field);
        }
        if (is_numeric($int) && !is_int($int)) {
            $int = intval($int);
        }
        if (!is_numeric($int) || !is_int($int)) {
            throw new Klarna_InvalidTypeException($field, "integer");
        }
    }

    /**
     * Checks/fixes so the VAT input is valid.
     *
     * @param float &$vat VAT.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkVAT(&$vat)
    {
        if (!isset($vat)) {
            throw new Klarna_ArgumentNotSetException("VAT");
        }
        if (is_numeric($vat) && (!is_int($vat) || !is_float($vat))) {
            $vat = floatval($vat);
        }
        if (!is_numeric($vat) || (!is_int($vat) && !is_float($vat))) {
            throw new Klarna_InvalidTypeException("VAT", "integer or float");
        }
    }

    /**
     * Checks/fixes so the amount input is valid.
     *
     * @param int &$amount Amount.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkAmount(&$amount)
    {
        if (!isset($amount)) {
            throw new Klarna_ArgumentNotSetException("Amount");
        }
        if (is_numeric($amount)) {
            $this->_fixValue($amount);
        }
        if (is_numeric($amount) && !is_int($amount)) {
            $amount = intval($amount);
        }
        if (!is_numeric($amount) || !is_int($amount)) {
            throw new Klarna_InvalidTypeException("amount", "integer");
        }
    }

    /**
     * Checks/fixes so the price input is valid.
     *
     * @param int &$price Price.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkPrice(&$price)
    {
        if (!isset($price)) {
            throw new Klarna_ArgumentNotSetException("Price");
        }
        if (is_numeric($price)) {
            $this->_fixValue($price);
        }
        if (is_numeric($price) && !is_int($price)) {
            $price = intval($price);
        }
        if (!is_numeric($price) || !is_int($price)) {
            throw new Klarna_InvalidTypeException("Price", "integer");
        }
    }

    /**
     * Multiplies value with 100 and rounds it.
     * This fixes value/price/amount inputs so that KO can handle them.
     *
     * @param float &$value value
     *
     * @return void
     */
    private function _fixValue(&$value)
    {
        $value = round($value * 100);
    }

    /**
     * Checks/fixes so the discount input is valid.
     *
     * @param float &$discount Discount amount.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkDiscount(&$discount)
    {
        if (!isset($discount)) {
            throw new Klarna_ArgumentNotSetException("Discount");
        }
        if (is_numeric($discount)
            && (!is_int($discount) || !is_float($discount))
        ) {
            $discount = floatval($discount);
        }

        if (!is_numeric($discount)
            || (!is_int($discount) && !is_float($discount))
        ) {
            throw new Klarna_InvalidTypeException("Discount", "integer or float");
        }
    }

    /**
     * Checks/fixes so that the estoreOrderNo input is valid.
     *
     * @param string &$estoreOrderNo Estores order number.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkEstoreOrderNo(&$estoreOrderNo)
    {
        if (!isset($estoreOrderNo)) {
            throw new Klarna_ArgumentNotSetException("Order number");
        }

        if (!is_string($estoreOrderNo)) {
            $estoreOrderNo = strval($estoreOrderNo);
            if (!is_string($estoreOrderNo)) {
                throw new Klarna_InvalidTypeException("Order number", "string");
            }
        }
    }

    /**
     * Checks/fixes to the PNO/SSN input is valid.
     *
     * @param string &$pno Personal number, social security  number, ...
     * @param int    $enc  {@link KlarnaEncoding PNO Encoding} constant.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkPNO(&$pno, $enc)
    {
        if (!$pno) {
            throw new Klarna_ArgumentNotSetException("PNO/SSN");
        }

        if (!KlarnaEncoding::checkPNO($pno)) {
            throw new Klarna_InvalidPNOException;
        }
    }

    /**
     * Checks/fixes to the country input is valid.
     *
     * @param int &$country {@link KlarnaCountry Country} constant.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkCountry(&$country)
    {
        if (!isset($country)) {
            throw new Klarna_ArgumentNotSetException("Country");
        }
        if (is_numeric($country) && !is_int($country)) {
            $country = intval($country);
        }
        if (!is_numeric($country) || !is_int($country)) {
            throw new Klarna_InvalidTypeException("Country", "integer");
        }
    }

    /**
     * Checks/fixes to the language input is valid.
     *
     * @param int &$language {@link KlarnaLanguage Language} constant.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkLanguage(&$language)
    {
        if (!isset($language)) {
            throw new Klarna_ArgumentNotSetException("Language");
        }
        if (is_numeric($language) && !is_int($language)) {
            $language = intval($language);
        }
        if (!is_numeric($language) || !is_int($language)) {
            throw new Klarna_InvalidTypeException("Language", "integer");
        }
    }

    /**
     * Checks/fixes to the currency input is valid.
     *
     * @param int &$currency {@link KlarnaCurrency Currency} constant.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkCurrency(&$currency)
    {
        if (!isset($currency)) {
            throw new Klarna_ArgumentNotSetException("Currency");
        }
        if (is_numeric($currency) && !is_int($currency)) {
            $currency = intval($currency);
        }
        if (!is_numeric($currency) || !is_int($currency)) {
            throw new Klarna_InvalidTypeException("Currency", "integer");
        }
    }

    /**
     * Checks/fixes so no/number is a valid input.
     *
     * @param int &$no Number.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkNo(&$no)
    {
        if (!isset($no)) {
            throw new Klarna_ArgumentNotSetException("no");
        }
        if (is_numeric($no) && !is_int($no)) {
            $no = intval($no);
        }
        if (!is_numeric($no) || !is_int($no) || $no <= 0) {
            throw new Klarna_InvalidTypeException('no', 'integer > 0');
        }
    }

    /**
     * Checks/fixes so reservation number is a valid input.
     *
     * @param string &$rno Reservation number.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkRNO(&$rno)
    {
        if (!is_string($rno)) {
            $rno = strval($rno);
        }
        if (strlen($rno) == 0) {
            throw new Klarna_ArgumentNotSetException("RNO");
        }
    }

    /**
     * Checks/fixes so that reference/refCode are valid.
     *
     * @param string &$reference Reference string.
     * @param string &$refCode   Reference code.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkRef(&$reference, &$refCode)
    {
        if (!is_string($reference)) {
            $reference = strval($reference);
            if (!is_string($reference)) {
                throw new Klarna_InvalidTypeException("Reference", "string");
            }
        }

        if (!is_string($refCode)) {
            $refCode = strval($refCode);
            if (!is_string($refCode)) {
                throw new Klarna_InvalidTypeException("Reference code", "string");
            }
        }
    }

    /**
     * Checks/fixes so that the OCR input is valid.
     *
     * @param string &$ocr OCR number.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkOCR(&$ocr)
    {
        if (!is_string($ocr)) {
            $ocr = strval($ocr);
            if (!is_string($ocr)) {
                throw new Klarna_InvalidTypeException("OCR", "string");
            }
        }
    }

     /**
     * Check so required argument is supplied.
     *
     * @param string $argument argument to check
     * @param string $name     name of argument
     *
     * @throws Klarna_ArgumentNotSetException
     * @return void
     */
    private function _checkArgument($argument, $name)
    {
        if (!is_string($argument)) {
            $argument = strval($argument);
        }

        if (strlen($argument) == 0) {
            throw new Klarna_ArgumentNotSetException($name);
        }
    }

    /**
     * Check so Locale settings (country, currency, language) are set.
     *
     * @throws KlarnaException
     * @return void
     */
    private function _checkLocale()
    {
        if (!is_int($this->_country)
            || !is_int($this->_language)
            || !is_int($this->_currency)
        ) {
            throw new Klarna_InvalidLocaleException;
        }
    }

    /**
     * Checks wether a goodslist is set.
     *
     * @throws Klarna_MissingGoodslistException
     * @return void
     */
    private function _checkGoodslist()
    {
        if (!is_array($this->goodsList) || empty($this->goodsList)) {
            throw new Klarna_MissingGoodslistException;
        }
    }

    /**
     * Set the pcStorage method used for this instance
     *
     * @param PCStorage $pcStorage PCStorage implementation
     *
     * @return void
     */
    public function setPCStorage($pcStorage)
    {
        if (!($pcStorage instanceof PCStorage)) {
            throw new Klarna_InvalidTypeException('pcStorage', 'PCStorage');
        }
        $this->pcStorage = $pcStorage->getName();
        $this->pclasses = $pcStorage;
    }

    /**
     * Ensure the configuration is of the correct type.
     *
     * @param array|ArrayAccess|null $config an optional config to validate
     *
     * @return void
     */
    private function _checkConfig($config = null)
    {
        if ($config === null) {
            $config = $this->config;
        }
        if (!($config instanceof ArrayAccess)
            && !is_array($config)
        ) {
            throw new Klarna_IncompleteConfigurationException;
        }
    }

} //End Klarna

/**
 * Include the {@link KlarnaConfig} class.
 */
require_once 'klarnaconfig.php';

/**
 * Include the {@link KlarnaPClass} class.
 */
require_once 'klarnapclass.php';

/**
 * Include the {@link KlarnaCalc} class.
 */
require_once 'klarnacalc.php';

/**
 * Include the {@link KlarnaAddr} class.
 */
require_once 'klarnaaddr.php';

/**
 * Include the Exception classes.
 */
require_once 'Exceptions.php';

/**
 * Include the KlarnaEncoding class.
 */
require_once 'Encoding.php';


/**
 * Include the KlarnaFlags class.
 */
require_once 'Flags.php';

/**
 * Include KlarnaCountry, KlarnaCurrency, KlarnaLanguage classes
 */
require_once 'Country.php';
require_once 'Currency.php';
require_once 'Language.php';
