* updated to support latest OTP api

This commit is contained in:
Danyi Dávid 2020-09-07 19:10:51 +02:00
parent c05715e905
commit d60f6c5d1b
9 changed files with 252 additions and 120 deletions

View File

@ -12,5 +12,7 @@ return [
'koin.pass' => '', 'koin.pass' => '',
'szep.card' => '', 'szep.card' => '',
'szep.key' => '', 'szep.birthDate' => '',
'szep.email' => '',
'szep.pass' => '',
]; ];

View File

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<StartWorkflow>
<TemplateName>BANKKARTYASZAMLAEGYENLEGLEKERDEZES</TemplateName>
<Variables>
<isClientCode>BANKKARTYASUGYFEL</isClientCode>
<isIdentificationData>%s</isIdentificationData>
<isSecretData>%s</isSecretData>
<isStartDate>%s</isStartDate>
<isEndDate>%s</isEndDate>
</Variables>
</StartWorkflow>

View File

@ -1,11 +0,0 @@
<SOAP-ENV:Envelope
xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<SOAP-ENV:Body>
<m:startWorkflowSynch xmlns:m="urn:MWAccess" SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<arg0 xsi:type="xsd:string">SZEPKARTYASZAMLATORTENET</arg0>
<arg1 xsi:type="xsd:string"><![CDATA[%s]]></arg1>
</m:startWorkflowSynch>
</SOAP-ENV:Body>
</SOAP-ENV:Envelope>

View File

@ -0,0 +1,8 @@
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:java="java:hu.iqsoft.otp.mw.access">
<soapenv:Header/>
<soapenv:Body>
<java:getWorkflowState>
<arg0>%s</arg0>
</java:getWorkflowState>
</soapenv:Body>
</soapenv:Envelope>

View File

@ -0,0 +1,9 @@
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:java="java:hu.iqsoft.otp.mw.access">
<soapenv:Header/>
<soapenv:Body>
<java:startWorkflowSynch>
<arg0>%s</arg0>
<arg1><![CDATA[%s]]></arg1>
</java:startWorkflowSynch>
</soapenv:Body>
</soapenv:Envelope>

View File

@ -0,0 +1,9 @@
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:java="java:hu.iqsoft.otp.mw.access">
<soapenv:Header/>
<soapenv:Body>
<java:startWorkflowMobilalkalmazas>
<arg0>%s</arg0>
<arg1><![CDATA[%s]]></arg1>
</java:startWorkflowMobilalkalmazas>
</soapenv:Body>
</soapenv:Envelope>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<StartWorkflow>
<TemplateName>%s</TemplateName>
<Variables>%s</Variables>
</StartWorkflow>

View File

@ -8,6 +8,7 @@ use App\Service\SZEPManagerService;
use Doctrine\ORM\OptimisticLockException; use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\ORMException; use Doctrine\ORM\ORMException;
use Doctrine\ORM\TransactionRequiredException; use Doctrine\ORM\TransactionRequiredException;
use Exception;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
@ -36,12 +37,10 @@ class PeriodicSZEPCommand extends Command
* @param InputInterface $input * @param InputInterface $input
* @param OutputInterface $output * @param OutputInterface $output
* @return int|null|void * @return int|null|void
* @throws ORMException * @throws Exception
* @throws OptimisticLockException
* @throws TransactionRequiredException
*/ */
protected function execute(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output)
{ {
$this->szepManager->pollRecent(); $this->szepManager->pollTransactions();
} }
} }

View File

@ -8,9 +8,6 @@ use App\Entity\SZEPCardEntry;
use DateInterval; use DateInterval;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityManager;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\ORMException;
use Doctrine\ORM\TransactionRequiredException;
use DOMDocument; use DOMDocument;
use DOMElement; use DOMElement;
use DOMXPath; use DOMXPath;
@ -19,18 +16,23 @@ use GuzzleHttp\Client;
class SZEPManagerService class SZEPManagerService
{ {
const TEMPLATE_WORKFLOW = "data/soap-xmls/SZEP_startWorkflowSynch.xml"; const TEMPLATE_SOAP_SYNC_WORKFLOW = "data/soap-xmls/soapMainSync_ws12.xml";
const TEMPLATE_QUERY_CARD = "data/soap-xmls/SZEP_queryCard.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 SOAP_ENDPOINT = "https://www.otpbankdirekt.hu/mwaccesspublic12/mwaccess";
const CERTIFICATE_CACHED_PATH = "data/cache/SZEP_cert.pem";
const SOAP_ENDPOINT = "https://www.otpbankdirekt.hu/mwaccesspublic1984/mwaccess";
const POCKET_FOOD = 'Vendéglátás'; const POCKET_FOOD = 'Vendéglátás';
const POCKET_SPORT = 'Szabadidő'; const POCKET_SPORT = 'Szabadidő';
const POCKET_LODGING = 'Szálláshely'; 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_FOOD = 'SZÉP kártya - étel';
const TAG_POCKET_SPORT = 'SZÉP kártya - szabadidő'; const TAG_POCKET_SPORT = 'SZÉP kártya - szabadidő';
const TAG_POCKET_LODGING = 'SZÉP kártya - szállás'; const TAG_POCKET_LODGING = 'SZÉP kártya - szállás';
@ -76,99 +78,201 @@ class SZEPManagerService
*/ */
public function pollBalance(): ?array public function pollBalance(): ?array
{ {
if (null !== ($pollResult = $this->getRecentXml())) { return $this->getBalance();
return $this->parseBalance($pollResult);
}
return null;
} }
/** /**
* @throws ORMException
* @throws OptimisticLockException
* @throws TransactionRequiredException
* @throws Exception * @throws Exception
*/ */
public function pollRecent() public function pollTransactions()
{ {
if (null !== ($pollResult = $this->getRecentXml())) { $this->getTransactions();
$this->parseResult($pollResult); }
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 <result>
* @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 * @return string
* @throws Exception * @throws Exception
*/ */
private function getRecentXml(): ?string private function getTransactions(): ?string
{ {
$certificate = $this->getCertificate(); $appToken = $this->getMobileApplicationToken();
openssl_public_encrypt($this->config['szep.card'], $cryptCardId, $certificate, OPENSSL_PKCS1_PADDING); $authToken = $this->getAuthToken($appToken);
openssl_public_encrypt($this->config['szep.key'], $cryptCardKey, $certificate, OPENSSL_PKCS1_PADDING);
$endDate = new DateTimeImmutable(); $endDate = new DateTimeImmutable();
$startDate = $endDate->sub(new DateInterval('P7D')); $startDate = $endDate->sub(new DateInterval('P14D'));
$action = 'SZEPAPP_LEKERDEZES_TRANZAKCIO';
$gueryTemplate = file_get_contents(self::TEMPLATE_QUERY_CARD); $parameters = [
$query = sprintf($gueryTemplate, 'isClientCode' => 'SZEPAPP',
"X" . base64_encode($cryptCardId), 'isCardNumber' => $this->config['szep.card'],
base64_encode($cryptCardKey), 'isMobilalkalmazasToken' => $appToken,
$startDate->format("Y.m.d"), 'isMobilalkalmazasAzonosito' => $this->config['szep.email'],
$endDate->format("Y.m.d") '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)); $transactionId = $this->getParsedSoapResponse($soapXml, '//return', false);
try{ $resultSetElement = $this->pollTransaction($transactionId, '//answer/resultset');
$soapResponseXml = $this->doSoapRequest($soapXml); $documentXpath = new DOMXPath($resultSetElement->ownerDocument);
} 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);
/** @var DOMElement[] $recordElements */ /** @var DOMElement[] $recordElements */
$recordElements = $documentXpath->query('/answer/resultset/record'); $recordElements = $documentXpath->query('//record', $resultSetElement);
$newRecords = []; $newRecords = [];
foreach ($recordElements as $element) { foreach ($recordElements as $element) {
$date = trim($documentXpath->query('./datum', $element)->item(0)->textContent); $transactionWay = trim($documentXpath
$amount = trim($documentXpath->query('./osszeg', $element)->item(0)->textContent); ->query('./transactionWay', $element)
$merchant = trim($documentXpath->query('./ellenoldali_nev', $element)->item(0)->textContent); ->item(0)
$pocket = trim($documentXpath->query('./alszamla', $element)->item(0)->textContent); ->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"); $hash = md5("$date#$amount#$merchant#$pocket");
if (null != $this->em->find(SZEPCardEntry::class, $hash)) { if (null != $this->em->find(SZEPCardEntry::class, $hash)) {
@ -215,9 +319,15 @@ class SZEPManagerService
{ {
$tags = []; $tags = [];
if (false !== strpos($SZEPCardEntry->getMerchant(), "Sigma Technology")) { if (false !== strpos($SZEPCardEntry->getMerchant(), "SIGMA TECHNOLOGY MAGYARORSZÁG INFOR")) {
$tags[] = 'Sigma'; $tags[] = 'Sigma';
$tags[] = 'Cafeteria'; $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")) { } elseif (false !== strpos($SZEPCardEntry->getMerchant(), "Ericsson Ház Étterem")) {
$tags[] = 'Ericsson Ház'; $tags[] = 'Ericsson Ház';
$tags[] = 'Menza'; $tags[] = 'Menza';
@ -253,6 +363,16 @@ class SZEPManagerService
return $tags; 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 * Returns AES encrypted base64 encoded $payload
* @param string $payload * @param string $payload
@ -271,7 +391,7 @@ class SZEPManagerService
* @param string $soapXml * @param string $soapXml
* @return string * @return string
*/ */
private function doSoapRequest(string $soapXml): string private function sendSoapRequest(string $soapXml): string
{ {
$response = $this->httpClient->post(self::SOAP_ENDPOINT, [ $response = $this->httpClient->post(self::SOAP_ENDPOINT, [
'body' => $soapXml, 'body' => $soapXml,
@ -279,18 +399,20 @@ class SZEPManagerService
return $response->getBody()->getContents(); return $response->getBody()->getContents();
} }
/** private function parseSoapResponse(string $soapResponseXml, string $selector, bool $decode): string
* Returns the cached public certificate
* @return string
*/
private function getCertificate(): string
{ {
if (file_exists(self::CERTIFICATE_CACHED_PATH)) { $domDocument = new DOMDocument();
return file_get_contents(self::CERTIFICATE_CACHED_PATH); $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); private function getParsedSoapResponse(string $soapXml, string $selector, bool $decode): string
file_put_contents(self::CERTIFICATE_CACHED_PATH, $cert); {
return $cert; $soapResponseXml = $this->sendSoapRequest($soapXml);
return $this->parseSoapResponse($soapResponseXml, $selector, $decode);
} }
} }