157 lines
5.4 KiB
PHP
157 lines
5.4 KiB
PHP
<?php
|
|
|
|
namespace Modules\Core\Shipping\Carriers\Elta;
|
|
|
|
use DOMDocument;
|
|
use DOMElement;
|
|
use DOMXPath;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Modules\Core\Shipping\Carriers\Elta\Exceptions\EltaApiException;
|
|
|
|
/**
|
|
* SOAP transport for ELTA Courier's web services.
|
|
*
|
|
* ELTA runs two parallel operation families at the same endpoint
|
|
* (212.205.47.226:9003). The original CREATEAWB/PELB64VG/PELTT01/PELSTATION
|
|
* set (sourced from the unofficial chinchillabrains/eltaws reference client)
|
|
* rejects this account's credentials with st_flag=3 ("invalid password").
|
|
* Decompiling ELTA's own official desktop client (ELTA_PEL.exe, from their
|
|
* public .msi installer) showed it never calls CREATEAWB at all — it
|
|
* exclusively uses a newer "*NEW" family (PELVGNEW, PELLOGINNEW,
|
|
* PELTTNEW01, PELTKNEW, PELVGDEL, PELPARALVGNEW1). PELVGNEW was
|
|
* live-verified directly against the production endpoint with this same
|
|
* account and succeeded (st_flag=0, real voucher number returned) — it
|
|
* requires no password at all, just the account/user code pair.
|
|
*
|
|
* No .wsdl files exist for the *NEW family (only reconstructed from the
|
|
* decompiled C# proxy classes' method signatures). PHP's SoapClient in
|
|
* non-WSDL mode refuses to serialize these calls at all (SoapFault "Error
|
|
* in client request message" regardless of style/use/SoapVar wrapping —
|
|
* this legacy Micro Focus/CICS gateway's document/literal shape doesn't
|
|
* match what ext-soap's non-WSDL client is willing to build), so *NEW
|
|
* operations are sent as hand-built SOAP 1.1 envelopes over plain HTTP —
|
|
* proven directly against the live endpoint — with the response parsed
|
|
* back via DOMDocument/DOMXPath.
|
|
*
|
|
* The old family's label operation (PELB64VG) is deliberately NOT
|
|
* exposed here — it fails with the same st_flag=3 as CREATEAWB (same
|
|
* legacy family), and has no *NEW replacement since ELTA's own client
|
|
* renders labels locally instead of fetching them (see
|
|
* EltaLabelRenderer).
|
|
*/
|
|
class EltaClient
|
|
{
|
|
public function __construct(private readonly array $config) {}
|
|
|
|
public function login(array $params): EltaResponse
|
|
{
|
|
return $this->call('PELLOGINNEW', $params);
|
|
}
|
|
|
|
public function createVoucher(array $params): EltaResponse
|
|
{
|
|
return $this->call('PELVGNEW', $params);
|
|
}
|
|
|
|
public function listVouchers(array $params): EltaResponse
|
|
{
|
|
return $this->call('PELPARALVGNEW1', $params);
|
|
}
|
|
|
|
public function deleteVoucher(array $params): EltaResponse
|
|
{
|
|
return $this->call('PELVGDEL', $params);
|
|
}
|
|
|
|
public function getTracking(array $params): EltaResponse
|
|
{
|
|
return $this->call('PELTTNEW01', $params);
|
|
}
|
|
|
|
public function getStation(array $params): EltaResponse
|
|
{
|
|
return $this->call('PELTKNEW', $params);
|
|
}
|
|
|
|
private function call(string $operation, array $params): EltaResponse
|
|
{
|
|
$namespace = '/'.$operation;
|
|
|
|
$body = '';
|
|
foreach ($params as $key => $value) {
|
|
$body .= '<cre:'.$key.'>'.htmlspecialchars((string) $value, ENT_XML1).'</cre:'.$key.'>';
|
|
}
|
|
|
|
$envelope = '<?xml version="1.0" encoding="UTF-8"?>'
|
|
.'<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:cre="'.$namespace.'">'
|
|
.'<soapenv:Header/><soapenv:Body><cre:READ>'.$body.'</cre:READ></soapenv:Body></soapenv:Envelope>';
|
|
|
|
$response = Http::withHeaders([
|
|
'SOAPAction' => 'Get',
|
|
'Content-Type' => 'text/xml',
|
|
])
|
|
->timeout($this->config['timeout'])
|
|
->withBody($envelope, 'text/xml')
|
|
->post($this->config['service_location']);
|
|
|
|
if ($response->failed()) {
|
|
throw new EltaApiException('ELTA request failed: HTTP '.$response->status());
|
|
}
|
|
|
|
return EltaResponse::fromSoapResult($this->parseEnvelope($response->body()));
|
|
}
|
|
|
|
private function parseEnvelope(string $xml): array
|
|
{
|
|
$dom = new DOMDocument();
|
|
|
|
if (! $dom->loadXML($xml)) {
|
|
throw new EltaApiException('ELTA returned a response that could not be parsed as XML.');
|
|
}
|
|
|
|
$xpath = new DOMXPath($dom);
|
|
$xpath->registerNamespace('soapenv', 'http://schemas.xmlsoap.org/soap/envelope/');
|
|
$readResponse = $xpath->query('//soapenv:Body/*')->item(0);
|
|
|
|
if (! $readResponse instanceof DOMElement) {
|
|
throw new EltaApiException('ELTA response did not contain a SOAP body.');
|
|
}
|
|
|
|
return $this->nodeToArray($readResponse);
|
|
}
|
|
|
|
private function nodeToArray(DOMElement $element): array
|
|
{
|
|
$data = [];
|
|
|
|
foreach ($element->childNodes as $child) {
|
|
if (! $child instanceof DOMElement) {
|
|
continue;
|
|
}
|
|
|
|
$name = $child->localName;
|
|
$hasElementChildren = false;
|
|
|
|
foreach ($child->childNodes as $grandchild) {
|
|
if ($grandchild instanceof DOMElement) {
|
|
$hasElementChildren = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
$value = $hasElementChildren ? $this->nodeToArray($child) : trim($child->textContent);
|
|
|
|
if (array_key_exists($name, $data)) {
|
|
if (! is_array($data[$name]) || ! array_is_list($data[$name])) {
|
|
$data[$name] = [$data[$name]];
|
|
}
|
|
$data[$name][] = $value;
|
|
} else {
|
|
$data[$name] = $value;
|
|
}
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
}
|