* 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' => '',
'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\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();
}
}

View File

@ -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 <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
* @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);
}
}