Feat: Adding Elta integrations
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user