OpenXE/www/plugins/liveimport/sofort/sofort.php
2021-05-21 08:49:41 +02:00

499 lines
13 KiB
PHP

<?php
/**
* Class sofort
* sofortimport for 'sofort.com'
*
* @see https://www.sofort.com/integrationCenter-ger-DE/content/view/full/3047/#h4-6
*/
class sofort
{
const COL_DIVIDER = ';';
const ROW_DIVIDER = "\r\n";
const DATE_FORMAT = 'Y-m-d';
const URL = 'https://api.sofort.com/api/xml';
const CONTENT_TYPE = 'application/xml; charset=UTF-8';
/**
* customer id as user name for API-access
*
* @var string
*/
private $customerID = '';
/**
* The API key
*
* Den API-Key können Sie im Anbietermenü unter Weitere Dienste -> API-Key einsehen.
*
* @var string
*/
private $api = '';
/**
* look n days into the past, max 30 days allowed!
*
* @var int
*/
private $days = 5;
/**
* Als Benutzername verwenden Sie bitte Ihre Kundennummer (bspw. 99999)
* als Passwort Ihren API-Key (bspw. a12b34cd567890123e456f7890123456)
*
* required credentials in array $zugangsdaten:
* array (
* 'API_USER' => 'customer id'
* 'API_KEY' => 'API key'
* )
*
* optional: 'API_DAYS'
*
* @param array $credentials
* @param string csv
*
* @throws Exception
* @throws RuntimeException
* @throws ResponseException
* @throws MissingArgumentException
*
* @return string
*/
public function Import(array $credentials)
{
$credentials = array_change_key_case($credentials, CASE_UPPER);
if (!array_key_exists('API_USER', $credentials)) {
throw new MissingArgumentException('API_USER fehlt');
}
$this->customerID = $credentials['API_USER'];
if (!array_key_exists('API_KEY', $credentials)) {
throw new MissingArgumentException('API_KEY fehlt');
}
$this->api = $credentials['API_KEY'];
if (array_key_exists('API_DAYS', $credentials)) {
$days = $credentials['API_DAYS'];
if (is_numeric($days)) {
$days = (int) $days;
$this->days = $days;
}
}
$csv = $this->importLoop();
$csv = implode(self::ROW_DIVIDER, $csv);
$csv = utf8_decode($csv);
return $csv;
}
/**
* copied and modified from 'ImportKontoauszug' in 'class.erpapi'
* used case "stripe" in switch statement
*
* @param string $csv the csv 'file' to import
* @param int $konto the konto id
* @param $app
* @return array($inserted, $duplicate);
*/
public function ImportKontoauszug($csv, $konto, $app)
{
$inserted = 0;
$duplicate = 0;
$db_array = preg_split("/(\r\n)+|(\n|\r)+/", $csv);
if (empty($db_array)) {
// empty db_array
return array($inserted, $duplicate);
}
// fix values
$gegenkonto = '';
$stamp = time();
$userName = $app->User->GetName();
$userName = mysqli_real_escape_string($app->DB->connection, $userName);
// skip first row -> 'header' line
$count = count($db_array);
for ($i = 1; $i < $count; $i++) {
// explode row
$row = $db_array[$i];
$row = str_replace('"','', $row);
$row = explode(self::COL_DIVIDER, $row);
// $csv="date;description;amount;currency\r\n";
// 0 date
// 1 description
// 2 amount
// 3 currency
// extract the values
list($buchung, $transaction, $vorgang, $projektID, $betrag, $waehrung,$gebuehr) = $row;
$buchung = mysqli_real_escape_string($app->DB->connection, $buchung);
$buchung = str_replace('"','', $buchung);
$vorgang = utf8_encode($vorgang );
$vorgang = mysqli_real_escape_string($app->DB->connection, $vorgang);
$vorgang = str_replace('"','',$vorgang);
$betrag = mysqli_real_escape_string($app->DB->connection, $betrag);
$waehrung = mysqli_real_escape_string($app->DB->connection, $waehrung);
$gebuehr = mysqli_real_escape_string($app->DB->connection, $gebuehr);
// haben vs. soll
list($haben, $soll) = $betrag > 0
? [$betrag, '']
: ['', $betrag];
// hash over some values
$pruefsumme = md5(serialize([$buchung, $vorgang, $soll, $haben, $waehrung]));
$check = $app->DB->Select("SELECT id FROM kontoauszuege WHERE buchung='$buchung' AND konto='$konto' AND pruefsumme='$pruefsumme' LIMIT 1");
if($check > 0) {
$duplicate++;
continue;
}
$sql = "INSERT INTO kontoauszuege (
konto,
buchung,
vorgang,
soll,
haben,
gebuehr,
waehrung,
fertig,
bearbeiter,
pruefsumme,
importgroup,
originalbuchung,
originalvorgang,
originalsoll,
originalhaben,
originalgebuehr,
originalwaehrung,
gegenkonto
) VALUE (
'$konto',
'$buchung',
'$vorgang',
'$soll',
'$haben',
'$gebuehr',
'$waehrung',
0,
'".$userName."',
'$pruefsumme',
'$stamp',
'$buchung',
'$vorgang',
'$soll',
'$haben',
'$gebuehr',
'$waehrung',
'$gegenkonto')";
$app->DB->Insert($sql);
$newid = $app->DB->GetInsertID();
$app->DB->Update("UPDATE kontoauszuege SET sort='$newid' WHERE id='$newid' LIMIT 1");
$inserted++;
}
return [$inserted, $duplicate];
}
/**
* perform curl request
* protected to override via child class, if necessary
*
* Sie müssen die korrekte URL aufrufen und dabei HTTPS als Protokoll verwenden.
* Sie müssen die korrekten Authentifizierungsinformationen übermitteln. Zur Authentifizierung
* wird die Basic-HTTP-Authentication (RFC 2617) verwendet.
*
* Sie müssen die korrekten Content-Type Header angeben.
* Ihre Daten müssen korrekt als XML formatiert sein (RFC 3023, siehe Parameterübersicht) und per HTTP POST
* verschickt werden.
*
* @param string $xml
* @return string
* @throws Exception
*/
protected function curl($xml)
{
$header = [
'Content-Type' => self::CONTENT_TYPE,
'Accept:' => self::CONTENT_TYPE,
];
$url = self::URL;
$ch = curl_init($url);
curl_setopt_array($ch, array(
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSLVERSION => 6,
CURLOPT_HTTPAUTH => CURLAUTH_BASIC,
CURLOPT_USERPWD => $this->getAuthString(),
CURLOPT_POSTFIELDS => $xml,
CURLOPT_HTTPHEADER => $header
));
$result = curl_exec($ch);
$info = curl_getinfo($ch);
curl_close($ch);
if (isset($info['http_code']) && $info['http_code'] !== 200) {
$code = $info['http_code'];
throw new RuntimeException(sprintf(
'Verbindung fehlgeschlagen - konnte \'%s\' nicht erreichen (Statuscode: \'%s\')', $url, $code
));
}
return $result;
}
/**
* perform the imports
*
* @throws Exception
* @throws RuntimeException
*
* @return array
*/
private function importLoop()
{
$page = 1;
$number = 20;
$end = $this->getDate();
$start = $this->getDate($this->days);
$csv = array('date'.self::COL_DIVIDER.'description'.self::COL_DIVIDER.'amount'.self::COL_DIVIDER.'currency');
do {
/**
* - product:
* Produkt, auf das die ausgegebenen Transaktionen eingeschränkt werden soll
* ("paycode": SOFORT Überweisung Paycode, "payment": SOFORT Überweisung)
* - status:
* Status, auf den die ausgegebenen Transaktionen eingeschränkt werden sollen
*
* status, reason, meaning
* loss not_credited Das Geld ist nicht eingegangen.
* pending not_credited_yet Das Geld ist noch nicht eingegangen.
* received credited Das Geld ist eingegangen.
* refunded compensation Das Geld wurde zurückerstattet (Teilrückbuchung).
* refunded refunded Das Geld wurde zurückerstattet (komplette Rückbuchung des Gesamtbetrags).
*
*/
$request = [
'from_time' => $start,
'to_time' => $end,
'number' => $number,
'page' => $page,
'product' => 'payment',
//'status' => 'received',
];
$xml = $this->generateXML('transaction_request version="2"', $request);
$response = $this->curl($xml);
if (empty($response)) {
break;
}
$xml = simplexml_load_string($response);
$childs = $xml->count();
$name = $xml->getName();
if ($name !== 'transactions') {
/*
* no 'transactions' response -> may an error or a warning?!
*
* <?xml version="1.0" encoding="UTF-8" ?>
* <errors>
* <error>
* <code>1000</code>
* <message>Invalid request.</message>
* </error>
* </errors>
*/
throw new ResponseException(sprintf(
'Expected \'transactions\' response, got \'%s\'\n%s', $name, $response
));
}
if (isset($xml->transaction_details)) {
// transaction_details found -> loop
foreach ($xml->transaction_details as $transaction) {
$csv[] = $this->extractData($transaction);
}
}
// loop as long as the received childs
// are the max. number of requested child
// so step to the next page
$page = $page + 1;
} while($childs === $number);
return $csv;
}
/**
* extract some data from the given transaction
* extract
* - date
* - amount
* - currency
* - description (transaction id, paycode, project id)
*
* @param SimpleXMLElement $xml
* @return string
*/
private function extractData(SimpleXMLElement $xml)
{
$transaction = (string) $xml->transaction;
$reasons = '';
foreach ($xml->reasons->reason as $index => $reason){
$reasons .= ' '.$reason;
}
$reasons = trim($reasons);
$date = (string) $xml->time;
$date = substr($date, 0, 10);
$amount = (float) $xml->amount;
$currency = (string) $xml->currency_code;
$paycode = (string) $xml->paycode->code;
$project = (string) $xml->project_id;
$costs = (float) $xml->costs->fees;
/*
* amount_refunded; [0,1]; Decimal (8.2); Zurück überwiesener Betrag
* [0,1] = optionaler Parameter, es kann max. ein Wert übergeben werden
*/
if (isset($xml->amount_refunded)) {
$amount_refunded = (float) $xml->amount_refunded;
if ($amount_refunded != 0) {
/*
* Make sure, the refunded amount is negative and replace the amount.
*/
$amount = -1 * abs($amount_refunded);
}
}
$line = [
$date,
$transaction,
$reasons,
$paycode.' '.$project,
$amount,
$currency,
$costs,
];
$line = implode(self::COL_DIVIDER, $line);
return $line;
}
/**
* generate the xml request from given array & tag
*
* e.g.
* $tag = 'transaction_request version="2"'
* $request = array(
* 'from_time' => '2013-04-01',
* 'to_time' => '2013-04-30',
* 'number' => 10,
* 'page' => 2,
* 'product' => 'paycode'
* );
*
* converts to
*
* <?xml version="1.0" encoding="UTF-8" ?>
* <transaction_request version="2">
* <from_time>2013-04-01</from_time>
* <to_time>2013-04-30</to_time>
* <product>paycode</product>
* <number>10</number>
* <page>2</page>
* </transaction_request>
*
*
* @param string $tag
* @param array $request
* @return string
*/
private function generateXML($tag, array $request)
{
// remove surrounding '< -- >' from tag
$tag = ltrim($tag, '<');
$tag = rtrim($tag, '>');
// explode the tag -> use first part as end tag (cut's version=2)
// e.g. $tag = 'transaction_request version="2"'
// converts to $end = transaction_request
$end = explode(' ', $tag)[0];
$xml = array();
$xml[] = '<?xml version="1.0" encoding="UTF-8" ?>';
$xml[] = "<{$tag}>";
foreach ($request as $k => $v){ $xml[] = "\t<{$k}>{$v}</{$k}>"; }
$xml[] = "</{$end}>";
$xml = implode("\n", $xml);
return $xml;
}
/**
* get the necessary Date
* if daysAgo is '0', return today's date
* else the date, n days ago
*
* @param int $daysAgo note: the max value is 29
*
* @throws RuntimeException
*
* @return string
*/
public function getDate($daysAgo = 0)
{
$daysAgo = (int)$daysAgo;
$daysAgo = (int) abs($daysAgo);
// allow max. 29 days
$daysAgo = min(29, $daysAgo);
if ($daysAgo === 0) {
$time = time();
} else {
$tmp = sprintf('-%s days', $daysAgo);
$time = strtotime($tmp);
}
// generate formatted date string from time
$date = date(self::DATE_FORMAT, $time);
if ($date === false) {
throw new RuntimeException(sprintf(
'Cannot create formatted date for %s day(s) ago.', $daysAgo
));
}
return $date;
}
/**
* Zur Authentifizierung wird die Basic-HTTP-Authentication (RFC 2617) verwendet.
* Als Benutzername verwenden Sie bitte Ihre Kundennummer (bspw. 99999) und als Passwort
* Ihren API-Key (bspw. a12b34cd567890123e456f7890123456), die Sie durch ":" getrennt
* aneinander fügen und mit Base64 codieren (base64(99999:a12b34cd567890123e456f7890123456)).
*
* @return string
*/
private function getAuthString()
{
return $this->customerID.':'.$this->api;
}
}