diff --git a/config/autoload/local.php.dist b/config/autoload/local.php.dist
index c35482a..b66dfb6 100644
--- a/config/autoload/local.php.dist
+++ b/config/autoload/local.php.dist
@@ -12,5 +12,7 @@ return [
'koin.pass' => '',
'szep.card' => '',
- 'szep.key' => '',
+ 'szep.birthDate' => '',
+ 'szep.email' => '',
+ 'szep.pass' => '',
];
diff --git a/data/soap-xmls/SZEP_queryCard.xml b/data/soap-xmls/SZEP_queryCard.xml
deleted file mode 100644
index b30a0aa..0000000
--- a/data/soap-xmls/SZEP_queryCard.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
- BANKKARTYASZAMLAEGYENLEGLEKERDEZES
-
- BANKKARTYASUGYFEL
- %s
- %s
- %s
- %s
-
-
\ No newline at end of file
diff --git a/data/soap-xmls/SZEP_startWorkflowSynch.xml b/data/soap-xmls/SZEP_startWorkflowSynch.xml
deleted file mode 100644
index ea15ddd..0000000
--- a/data/soap-xmls/SZEP_startWorkflowSynch.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
- SZEPKARTYASZAMLATORTENET
-
-
-
-
diff --git a/data/soap-xmls/pollMain_ws12.xml b/data/soap-xmls/pollMain_ws12.xml
new file mode 100644
index 0000000..c92bcc2
--- /dev/null
+++ b/data/soap-xmls/pollMain_ws12.xml
@@ -0,0 +1,8 @@
+
+
+
+
+ %s
+
+
+
diff --git a/data/soap-xmls/soapMainSync_ws12.xml b/data/soap-xmls/soapMainSync_ws12.xml
new file mode 100644
index 0000000..fcb43f7
--- /dev/null
+++ b/data/soap-xmls/soapMainSync_ws12.xml
@@ -0,0 +1,9 @@
+
+
+
+
+%s
+
+
+
+
diff --git a/data/soap-xmls/soapMainWithMobilToken_ws12.xml b/data/soap-xmls/soapMainWithMobilToken_ws12.xml
new file mode 100644
index 0000000..0f597be
--- /dev/null
+++ b/data/soap-xmls/soapMainWithMobilToken_ws12.xml
@@ -0,0 +1,9 @@
+
+
+
+
+ %s
+
+
+
+
diff --git a/data/soap-xmls/soapPayload.xml b/data/soap-xmls/soapPayload.xml
new file mode 100644
index 0000000..ee30741
--- /dev/null
+++ b/data/soap-xmls/soapPayload.xml
@@ -0,0 +1,5 @@
+
+
+ %s
+ %s
+
\ No newline at end of file
diff --git a/src/App/Command/PeriodicSZEPCommand.php b/src/App/Command/PeriodicSZEPCommand.php
index 43d0077..8bd6180 100644
--- a/src/App/Command/PeriodicSZEPCommand.php
+++ b/src/App/Command/PeriodicSZEPCommand.php
@@ -8,6 +8,7 @@ use App\Service\SZEPManagerService;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\ORMException;
use Doctrine\ORM\TransactionRequiredException;
+use Exception;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@@ -36,12 +37,10 @@ class PeriodicSZEPCommand extends Command
* @param InputInterface $input
* @param OutputInterface $output
* @return int|null|void
- * @throws ORMException
- * @throws OptimisticLockException
- * @throws TransactionRequiredException
+ * @throws Exception
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
- $this->szepManager->pollRecent();
+ $this->szepManager->pollTransactions();
}
}
diff --git a/src/App/Service/SZEPManagerService.php b/src/App/Service/SZEPManagerService.php
index da098ca..360d218 100644
--- a/src/App/Service/SZEPManagerService.php
+++ b/src/App/Service/SZEPManagerService.php
@@ -8,9 +8,6 @@ use App\Entity\SZEPCardEntry;
use DateInterval;
use DateTimeImmutable;
use Doctrine\ORM\EntityManager;
-use Doctrine\ORM\OptimisticLockException;
-use Doctrine\ORM\ORMException;
-use Doctrine\ORM\TransactionRequiredException;
use DOMDocument;
use DOMElement;
use DOMXPath;
@@ -19,18 +16,23 @@ use GuzzleHttp\Client;
class SZEPManagerService
{
- const TEMPLATE_WORKFLOW = "data/soap-xmls/SZEP_startWorkflowSynch.xml";
- const TEMPLATE_QUERY_CARD = "data/soap-xmls/SZEP_queryCard.xml";
+ const TEMPLATE_SOAP_SYNC_WORKFLOW = "data/soap-xmls/soapMainSync_ws12.xml";
+ const TEMPLATE_SOAP_MOBILE_WORKFLOW = "data/soap-xmls/soapMainWithMobilToken_ws12.xml";
+ const TEMPLATE_SOAP_PAYLOAD = "data/soap-xmls/soapPayload.xml";
+ const TEMPLATE_POLL_TRANSACTION = "data/soap-xmls/pollMain_ws12.xml";
- const CERTIFICATE_WEB_PATH = "https://www.otpbankdirekt.hu/homebank/mobilalkalmazas/certificate";
- const CERTIFICATE_CACHED_PATH = "data/cache/SZEP_cert.pem";
-
- const SOAP_ENDPOINT = "https://www.otpbankdirekt.hu/mwaccesspublic1984/mwaccess";
+ const SOAP_ENDPOINT = "https://www.otpbankdirekt.hu/mwaccesspublic12/mwaccess";
const POCKET_FOOD = 'Vendéglátás';
const POCKET_SPORT = 'Szabadidő';
const POCKET_LODGING = 'Szálláshely';
+ const POCKET_MAP = [
+ 7 => self::POCKET_FOOD,
+ 8 => self::POCKET_SPORT,
+ 9 => self::POCKET_LODGING,
+ ];
+
const TAG_POCKET_FOOD = 'SZÉP kártya - étel';
const TAG_POCKET_SPORT = 'SZÉP kártya - szabadidő';
const TAG_POCKET_LODGING = 'SZÉP kártya - szállás';
@@ -76,99 +78,201 @@ class SZEPManagerService
*/
public function pollBalance(): ?array
{
- if (null !== ($pollResult = $this->getRecentXml())) {
- return $this->parseBalance($pollResult);
- }
- return null;
+ return $this->getBalance();
}
/**
- * @throws ORMException
- * @throws OptimisticLockException
- * @throws TransactionRequiredException
* @throws Exception
*/
- public function pollRecent()
+ public function pollTransactions()
{
- if (null !== ($pollResult = $this->getRecentXml())) {
- $this->parseResult($pollResult);
+ $this->getTransactions();
+ }
+
+ private function buildPayload(string $action, array $parameters): string
+ {
+ $template = file_get_contents(self::TEMPLATE_SOAP_PAYLOAD);
+ $values = "";
+ foreach ($parameters as $key => $value) {
+ $values .= sprintf('<%1$s>%2$s%1$s>', $key, $value);
}
+ return sprintf($template, $action, $values);
+ }
+
+ /**
+ * Poll a transaction and return its
+ * @param string $transactionId
+ * @param string $selector
+ * @return DOMElement
+ */
+ private function pollTransaction(string $transactionId, string $selector): DOMElement
+ {
+ $soapResponse = $this->sendSoapRequest(sprintf(file_get_contents(self::TEMPLATE_POLL_TRANSACTION), $transactionId));
+ while ('true' != $this->parseSoapResponse($soapResponse, '//return/m:Completed', false)) {
+ sleep(1);
+ $soapResponse = $this->sendSoapRequest(sprintf(file_get_contents(self::TEMPLATE_POLL_TRANSACTION), $transactionId));
+ }
+ $pollResultXml = $this->parseSoapResponse($soapResponse, '//return/m:Result', true);
+ $domDocument = new DOMDocument();
+ $domDocument->loadXML($pollResultXml);
+ $documentXpath = new DOMXPath($domDocument);
+ /** @var DOMElement $returnElement */
+ $returnElement = $documentXpath->query($selector)->item(0);
+ return $returnElement;
+ }
+
+ private function fetchMobileApplicationToken(string $cardNo, string $birthDate): string
+ {
+ $action = 'SZEPAPP_REGISZTRACIO';
+ $parameters = [
+ 'isClientCode' => 'SZEPAPP',
+ 'isOperation' => 'CHECK',
+ 'isCardNumber' => $cardNo,
+ 'isBirthDate' => $birthDate,
+ ];
+ $soapXml = sprintf(
+ file_get_contents(self::TEMPLATE_SOAP_SYNC_WORKFLOW),
+ $action,
+ $this->encryptPayload($this->buildPayload($action, $parameters))
+ );
+
+ $soapResponseXml = $this->getParsedSoapResponse($soapXml, '//return/m:Result', true);
+ $domDocument = new DOMDocument();
+ $domDocument->loadXML($soapResponseXml);
+ $documentXpath = new DOMXPath($domDocument);
+ $documentXpath->registerNamespace('m',"java:hu.iqsoft.otp.mw.access");
+ /** @var DOMElement $returnElement */
+ $returnElement = $documentXpath->query('//answer/mobilalkalmazasToken')->item(0);
+ return $returnElement->textContent;
+ }
+
+ private function getMobileApplicationToken(): string
+ {
+ return $this->fetchMobileApplicationToken($this->config['szep.card'],
+ $this->config['szep.birthDate']);
+ }
+
+ private function fetchAuthToken(string $appToken, string $email, string $pass): ?string
+ {
+ $action = 'SZEPAPP_JELSZO';
+ $hashedPass = $this->hashString($pass);
+ $parameters = [
+ 'isClientCode' => 'SZEPAPP',
+ 'isOperation' => 'CHECK',
+ 'isMobilalkalmazasToken' => $appToken,
+ 'isMobilalkalmazasAzonosito' => $email,
+ 'isEmail' => $this->hashString($email),
+ 'isPassword' => $hashedPass,
+ 'isOldPassword' => $hashedPass,
+ ];
+ $soapXml = sprintf(
+ file_get_contents(self::TEMPLATE_SOAP_MOBILE_WORKFLOW),
+ $action,
+ $this->encryptPayload($this->buildPayload($action, $parameters))
+ );
+ $transactionId = $this->getParsedSoapResponse($soapXml, '//return', false);
+
+ return $this->pollTransaction($transactionId, '//answer/authToken')->textContent;
+ }
+
+ private function getAuthToken(string $appToken): ?string
+ {
+ return $this->fetchAuthToken($appToken,
+ $this->config['szep.email'],
+ $this->config['szep.pass']);
+ }
+
+ private function getBalance(): ?array
+ {
+ $appToken = $this->getMobileApplicationToken();
+ $authToken = $this->getAuthToken($appToken);
+ $action = 'SZEPAPP_LEKERDEZES_KARTYA';
+ $parameters = [
+ 'isClientCode' => 'SZEPAPP',
+ 'isCardType' => 'S',
+ 'isMobilalkalmazasToken' => $appToken,
+ 'isMobilalkalmazasAzonosito' => $this->config['szep.email'],
+ 'isAuthToken' => $authToken,
+ ];
+ $soapXml = sprintf(
+ file_get_contents(self::TEMPLATE_SOAP_MOBILE_WORKFLOW),
+ $action,
+ $this->encryptPayload($this->buildPayload($action, $parameters))
+ );
+ $transactionId = $this->getParsedSoapResponse($soapXml, '//return', false);
+ $resultXml = $this->pollTransaction($transactionId, '//answer/resultset');
+ $documentXpath = new DOMXPath($resultXml->ownerDocument);
+ $activeRecord = $documentXpath->query('//record/status[text()="A"]/parent::record', $resultXml)->item(0);
+ $pocketSelector = '//pocketCode[text()="%d"]/parent::balanceRec/balance';
+ return [
+ 'vendeglatas' => intval($documentXpath->query(sprintf($pocketSelector, 7), $activeRecord)->item(0)->textContent),
+ 'szabadido' => intval($documentXpath->query(sprintf($pocketSelector, 8), $activeRecord)->item(0)->textContent),
+ 'szallashely' => intval($documentXpath->query(sprintf($pocketSelector, 9), $activeRecord)->item(0)->textContent),
+ ];
}
/**
* @return string
* @throws Exception
*/
- private function getRecentXml(): ?string
+ private function getTransactions(): ?string
{
- $certificate = $this->getCertificate();
- openssl_public_encrypt($this->config['szep.card'], $cryptCardId, $certificate, OPENSSL_PKCS1_PADDING);
- openssl_public_encrypt($this->config['szep.key'], $cryptCardKey, $certificate, OPENSSL_PKCS1_PADDING);
+ $appToken = $this->getMobileApplicationToken();
+ $authToken = $this->getAuthToken($appToken);
$endDate = new DateTimeImmutable();
- $startDate = $endDate->sub(new DateInterval('P7D'));
-
- $gueryTemplate = file_get_contents(self::TEMPLATE_QUERY_CARD);
- $query = sprintf($gueryTemplate,
- "X" . base64_encode($cryptCardId),
- base64_encode($cryptCardKey),
- $startDate->format("Y.m.d"),
- $endDate->format("Y.m.d")
+ $startDate = $endDate->sub(new DateInterval('P14D'));
+ $action = 'SZEPAPP_LEKERDEZES_TRANZAKCIO';
+ $parameters = [
+ 'isClientCode' => 'SZEPAPP',
+ 'isCardNumber' => $this->config['szep.card'],
+ 'isMobilalkalmazasToken' => $appToken,
+ 'isMobilalkalmazasAzonosito' => $this->config['szep.email'],
+ 'isAuthToken' => $authToken,
+ 'isTransactionCount' => 100,
+ 'isStartingDate' => $startDate->format("Y.m.d"),
+ 'isClosingDate' => $endDate->format("Y.m.d"),
+ 'isAmountFrom' => 0,
+ 'isAmountTo' => 999999,
+ ];
+ $soapXml = sprintf(
+ file_get_contents(self::TEMPLATE_SOAP_MOBILE_WORKFLOW),
+ $action,
+ $this->encryptPayload($this->buildPayload($action, $parameters))
);
- $soapXml = sprintf(file_get_contents(self::TEMPLATE_WORKFLOW), $this->encryptPayload($query));
- try{
- $soapResponseXml = $this->doSoapRequest($soapXml);
- } catch (Exception $e) {
- return null;
- }
-
- $domDocument = new DOMDocument();
- $domDocument->loadXML($soapResponseXml);
-
- $documentXpath = new DOMXPath($domDocument);
- /** @var DOMElement $returnElement */
- $returnElement = $documentXpath->query('//return/result')->item(0);
- return base64_decode($returnElement->textContent);
- }
-
- private function parseBalance(string $resultXml): array
- {
- $domDocument = new DOMDocument();
- $domDocument->loadXML($resultXml);
-
- $documentXpath = new DOMXPath($domDocument);
- return [
- 'vendeglatas' => intval($documentXpath->query('/answer/szamla_osszeg7')->item(0)->textContent),
- 'szabadido' => intval($documentXpath->query('/answer/szamla_osszeg8')->item(0)->textContent),
- 'szallashely' => intval($documentXpath->query('/answer/szamla_osszeg9')->item(0)->textContent),
- ];
- }
-
- /**
- * Parse the decoded payload
- * @param string $resultXml
- * @throws ORMException
- * @throws OptimisticLockException
- * @throws TransactionRequiredException
- * @throws Exception
- */
- private function parseResult(string $resultXml)
- {
- $domDocument = new DOMDocument();
- $domDocument->loadXML($resultXml);
-
- $documentXpath = new DOMXPath($domDocument);
+ $transactionId = $this->getParsedSoapResponse($soapXml, '//return', false);
+ $resultSetElement = $this->pollTransaction($transactionId, '//answer/resultset');
+ $documentXpath = new DOMXPath($resultSetElement->ownerDocument);
/** @var DOMElement[] $recordElements */
- $recordElements = $documentXpath->query('/answer/resultset/record');
+ $recordElements = $documentXpath->query('//record', $resultSetElement);
$newRecords = [];
-
foreach ($recordElements as $element) {
- $date = trim($documentXpath->query('./datum', $element)->item(0)->textContent);
- $amount = trim($documentXpath->query('./osszeg', $element)->item(0)->textContent);
- $merchant = trim($documentXpath->query('./ellenoldali_nev', $element)->item(0)->textContent);
- $pocket = trim($documentXpath->query('./alszamla', $element)->item(0)->textContent);
+ $transactionWay = trim($documentXpath
+ ->query('./transactionWay', $element)
+ ->item(0)
+ ->textContent);
+ $directionMultiplier = $transactionWay == 'J' ? 1 : -1;
+ $date = trim($documentXpath
+ ->query('./date', $element)
+ ->item(0)
+ ->textContent);
+ /**
+ * TODO: some dumbass called this parameter "ammount"
+ */
+ $amount = $directionMultiplier * intval($documentXpath
+ ->query('./ammount', $element)
+ ->item(0)
+ ->textContent);
+ $merchant = trim($documentXpath
+ ->query('./beneficiaryName', $element)
+ ->item(0)
+ ->textContent);
+ $pocket = self::POCKET_MAP[intval($documentXpath
+ ->query('./pocketCode', $element)
+ ->item(0)
+ ->textContent)];
$hash = md5("$date#$amount#$merchant#$pocket");
if (null != $this->em->find(SZEPCardEntry::class, $hash)) {
@@ -215,9 +319,15 @@ class SZEPManagerService
{
$tags = [];
- if (false !== strpos($SZEPCardEntry->getMerchant(), "Sigma Technology")) {
+ if (false !== strpos($SZEPCardEntry->getMerchant(), "SIGMA TECHNOLOGY MAGYARORSZÁG INFOR")) {
$tags[] = 'Sigma';
$tags[] = 'Cafeteria';
+ } elseif (false !== strpos($SZEPCardEntry->getMerchant(), "Hong He Kinai Büfé")) {
+ $tags[] = 'Junk food';
+ $tags[] = 'Kínai';
+ } elseif (false !== strpos($SZEPCardEntry->getMerchant(), "VIN.VIN étterem")) {
+ $tags[] = 'Junk food';
+ $tags[] = 'Vietnámi';
} elseif (false !== strpos($SZEPCardEntry->getMerchant(), "Ericsson Ház Étterem")) {
$tags[] = 'Ericsson Ház';
$tags[] = 'Menza';
@@ -253,6 +363,16 @@ class SZEPManagerService
return $tags;
}
+ /**
+ * Return sha512 hash of the md5sum of the input string
+ * @param string $input
+ * @return string
+ */
+ private function hashString(string $input): string
+ {
+ return strtoupper(openssl_digest(strtoupper(md5($input)), 'sha512'));
+ }
+
/**
* Returns AES encrypted base64 encoded $payload
* @param string $payload
@@ -271,7 +391,7 @@ class SZEPManagerService
* @param string $soapXml
* @return string
*/
- private function doSoapRequest(string $soapXml): string
+ private function sendSoapRequest(string $soapXml): string
{
$response = $this->httpClient->post(self::SOAP_ENDPOINT, [
'body' => $soapXml,
@@ -279,18 +399,20 @@ class SZEPManagerService
return $response->getBody()->getContents();
}
- /**
- * Returns the cached public certificate
- * @return string
- */
- private function getCertificate(): string
+ private function parseSoapResponse(string $soapResponseXml, string $selector, bool $decode): string
{
- if (file_exists(self::CERTIFICATE_CACHED_PATH)) {
- return file_get_contents(self::CERTIFICATE_CACHED_PATH);
- }
+ $domDocument = new DOMDocument();
+ $domDocument->loadXML($soapResponseXml);
+ $documentXpath = new DOMXPath($domDocument);
+ $documentXpath->registerNamespace('m',"java:hu.iqsoft.otp.mw.access");
+ /** @var DOMElement $returnElement */
+ $returnElement = $documentXpath->query($selector)->item(0);
+ return $decode ? base64_decode($returnElement->textContent) : $returnElement->textContent;
+ }
- $cert = file_get_contents(self::CERTIFICATE_WEB_PATH);
- file_put_contents(self::CERTIFICATE_CACHED_PATH, $cert);
- return $cert;
+ private function getParsedSoapResponse(string $soapXml, string $selector, bool $decode): string
+ {
+ $soapResponseXml = $this->sendSoapRequest($soapXml);
+ return $this->parseSoapResponse($soapResponseXml, $selector, $decode);
}
}