Open source / Delivery

ux2dev/econt

The Econt e-delivery API. All 25 RPC operations across seven service groups, generated from Econt's own OpenAPI spec so upstream changes are a regeneration.

mainPHP 8.2+MITLaravel ready
$ composer require ux2dev/econt
GitHub ↗

Framework-agnostic PHP SDK for the Econt e-deliveryexpress API. Covers all 25 RPC operations across 7 service groups (Nomenclatures, Address, Labels, Shipments, Profile, ThreeWayLogistics, PaymentReport) as resource methods that take typed Input DTOs and return typed Result DTOs.

Ships with three opt-in layers: framework-agnostic core, Laravel adapter (multi-account manager + facade), and a persistence module (Eloquent models, repository contracts, sync/polling actions, queueable jobs, Form Requests).

The full DTO + resource catalogue is generated from Econt's openapi.yaml — re-run the generator to pick up upstream changes.

Requirements

  • PHP 8.2 or higher
  • JSON extension
  • A PSR-18 HTTP client and PSR-17 request/stream factories (Guzzle provides both)

Installation

composer require ux2dev/econt

Quick start

The SDK takes a PSR-18 HTTP client and two PSR-17 factories — no specific implementation is required. Any PSR-18 client works (Guzzle, Symfony HttpClient, Buzz, kriswallsmith/Buzz, your own…). The example below uses Guzzle because it satisfies both contracts in a single package; bring whatever HTTP stack your project already has.

use Psr\Http\Client\ClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Ux2Dev\Econt\Config\EcontConfig;
use Ux2Dev\Econt\Dto\Input\Nomenclatures\GetCountriesInput;
use Ux2Dev\Econt\Econt;
use Ux2Dev\Econt\Enum\Environment;

/** @var ClientInterface $httpClient */            // any PSR-18 client
/** @var RequestFactoryInterface $requestFactory */ // any PSR-17 factory
/** @var StreamFactoryInterface  $streamFactory */  // any PSR-17 factory

$config = new EcontConfig(
    username:    'your-econt-user',
    password:    'your-econt-password',
    environment: Environment::Demo,
);

$econt = new Econt($config, $httpClient, $requestFactory, $streamFactory);

$result = $econt->nomenclatures()->getCountries(new GetCountriesInput());

foreach ($result->countries as $country) {
    echo $country->data['code2'] . PHP_EOL;
}

Concrete setup with Guzzle (one extra dep, satisfies PSR-18 + both PSR-17 factories):

composer require guzzlehttp/guzzle
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\HttpFactory;

$factory = new HttpFactory();
$econt = new Econt($config, new Client(), $factory, $factory);

Or — if you're on Symfony — pair symfony/http-client (PSR-18) with nyholm/psr7 (PSR-17 factories). The SDK doesn't care which.

Laravel

The package auto-discovers Ux2Dev\Econt\Laravel\EcontServiceProvider. Publish the config:

php artisan vendor:publish --tag=econt-config

config/econt.php defines accounts. Set the default account's credentials in .env:

ECONT_USERNAME=your-econt-user
ECONT_PASSWORD=your-econt-password
ECONT_ENVIRONMENT=demo

Use the facade:

use Ux2Dev\Econt\Dto\Input\Nomenclatures\GetCountriesInput;
use Ux2Dev\Econt\Laravel\Facades\Econt;

$result = Econt::nomenclatures()->getCountries(new GetCountriesInput());

Multiple accounts: add entries to accounts and switch at runtime:

// config/econt.php
'accounts' => [
    'main' => [...],
    'eu'   => [...],
],

// at runtime
Econt::account('eu')->labels()->createLabel(...);

Each account is resolved once and cached for the request lifetime. The container also resolves app(Econt::class) to the default account.

If your app already binds Psr\Http\Client\ClientInterface, RequestFactoryInterface, or StreamFactoryInterface, the provider respects those bindings. Otherwise it falls back to Guzzle.

Persistence module (opt-in)

The package ships an optional Laravel module that mirrors Econt's nomenclatures (countries, cities, offices, streets, quarters) into local tables for fast offline lookups. It is not auto-discovered.

Enable:

  1. Publish the migrations:

    php artisan vendor:publish --tag=econt-migrations
    php artisan migrate
    
  2. Toggle the gate in .env:

    ECONT_PERSISTENCE_ENABLED=true
    
  3. Register the provider in bootstrap/providers.php:

    return [
        App\Providers\AppServiceProvider::class,
        Ux2Dev\Econt\Laravel\Persistence\PersistenceServiceProvider::class,
    ];
    

Sync:

# All nomenclatures
php artisan econt:sync-nomenclatures

# Specific areas, on a non-default account
php artisan econt:sync-nomenclatures --account=eu --only=countries --only=cities

Or queue it:

use Ux2Dev\Econt\Laravel\Persistence\Jobs\SyncNomenclaturesJob;

SyncNomenclaturesJob::dispatch(account: 'eu', only: ['countries', 'cities']);

Shipment tracking:

After a successful Econt::labels()->createLabel(...), persist the shipment so subsequent polls find it:

use Ux2Dev\Econt\Dto\Input\Shipments\CreateLabelInput;
use Ux2Dev\Econt\Laravel\Facades\Econt;
use Ux2Dev\Econt\Laravel\Persistence\Actions\RecordShipmentCreationAction;

$result = Econt::labels()->createLabel(new CreateLabelInput(...));
app(RecordShipmentCreationAction::class)('main', $result);

Schedule the polling command in routes/console.php:

use Illuminate\Support\Facades\Schedule;

Schedule::command('econt:poll-statuses')->everyFifteenMinutes();

Or queue it on demand:

use Ux2Dev\Econt\Laravel\Persistence\Jobs\PollShipmentStatusesJob;

PollShipmentStatusesJob::dispatch(account: 'main', limit: 500, since: '30 days');

econt_shipment_statuses only grows when the latest status differs from the most recent stored row, so repeated polls are cheap.

Repositories:

Each aggregate has a contract-bound repository:

use Ux2Dev\Econt\Laravel\Persistence\Repositories\Contracts\CityRepository;
use Ux2Dev\Econt\Laravel\Persistence\Repositories\Contracts\CountryRepository;

$bg = app(CountryRepository::class)->findByEcontId(1);
$matches = app(CityRepository::class)->search('Sofia', limit: 10);

Override any binding via config('econt.persistence.repositories'):

'repositories' => [
    \Ux2Dev\Econt\Laravel\Persistence\Repositories\Contracts\CountryRepository::class
        => App\Econt\Repositories\CachedCountryRepository::class,
],

Form Requests:

Five publishable Form Requests validate inbound HTTP payloads against Econt's required fields and produce typed core-SDK Input DTOs:

use Ux2Dev\Econt\Laravel\Facades\Econt;
use Ux2Dev\Econt\Laravel\Persistence\Http\Requests\CreateLabelRequest;
use Ux2Dev\Econt\Laravel\Persistence\Actions\RecordShipmentCreationAction;

public function store(CreateLabelRequest $request)
{
    $result = Econt::labels()->createLabel($request->toInput());
    app(RecordShipmentCreationAction::class)('main', $result);
    return back()->with('awb', $result->label?->data['shipmentNumber'] ?? null);
}

Available: CreateLabelRequest, UpdateLabelRequest, DeleteLabelsRequest, RequestCourierRequest, ValidateAddressRequest.

Validation messages live under the econt translation namespace. Publish to override:

php artisan vendor:publish --tag=econt-translations

Configuration

EcontConfig is a final readonly value object. Validates at construction; the password is private and redacted from var_dump(); serialization is blocked.

new EcontConfig(
    username:    'KZ12345',
    password:    '...',
    environment: Environment::Production,  // or Environment::Demo
    timeout:     30,                       // optional, seconds
);

The Environment enum carries the base URL — no free-form URLs to typo. A baseUrlOverride parameter exists for tests/proxies only.

Resources

Seven resources, one method per RPC action:

Resource Methods
nomenclatures() getCountries, getCities, getOffices, getStreets, getQuarters
address() validateAddress, addressServiceTimes, getNearestOffices
labels() createLabel, createLabels, deleteLabels, updateLabel, updateLabels, checkPossibleShipmentEditions, grouping, groupingCancelation
shipments() requestCourier, getShipmentStatuses, getRequestCourierStatus, getMyAWB, setITUCode
profile() getClientProfiles, createCDAgreement
threeWayLogistics() threeWayLogistics
paymentReport() paymentReport

Every method takes a single *Input DTO and returns the matching *Result DTO. Nested objects share Dto/Input/Common/ and Dto/Result/Common/ sub-DTOs.

Exceptions

All SDK exceptions extend Ux2Dev\Econt\Exception\EcontException:

Exception Trigger
ConfigurationException Invalid EcontConfig (empty creds, non-positive timeout, etc.)
TransportException PSR-18 client throws (DNS, socket, TLS, timeout)
AuthenticationException HTTP 401 — Econt rejected credentials
ApiException HTTP 4xx/5xx, or 200 with an error envelope
InvalidResponseException 2xx but body is not valid JSON object

Security

This package is a wrapper, not an application. The host app is responsible for the surrounding controls.

  • Form Requests as scaffolding. Ux2Dev\Econt\Laravel\Persistence\Http\Requests\* ship with authorize(): true so they are usable in any host. Do not bind them to public routes unmodified. Either subclass and implement an ownership / policy check (auth()->user()->can('econt.manage')) or wrap the corresponding routes in auth + can: middleware. Calling Econt::shipments()->createLabel(...) costs money — treat it as a privileged action.
  • PII at rest. econt_shipments stores sender/receiver name, phone, and address. Set ECONT_PERSISTENCE_ENCRYPT_PII=true to enable Laravel's encrypted cast on those columns and on raw_response_json. Requires APP_KEY to be present and stable. On an existing populated database, you must re-write rows after enabling.
  • Logging exceptions. AuthenticationException exposes only the path and the response body size — the body itself is dropped at construction time so accidentally Log::error($e) can't dump credential hints. InvalidResponseException::rawBody() is truncated to 500 chars but still returns server output; prefer logging getMessage() + path() only. The transport masks Authorization headers in any wrapped client exception message.
  • Credentials in queue payloads. EcontConfig blocks serialize() / unserialize() outright, so it can never end up in a job payload. Jobs in this package (PollShipmentStatusesJob, SyncNomenclaturesJob) accept only scalar arguments and validate them in the constructor.

Updating to a new Econt API version

curl -sSL https://ee.econt.com/services/openapi.yaml > bin/openapi.yaml
php bin/generate.php
git diff
git commit

The generator wipes and rewrites src/Dto/ and src/Resources/. Hand-written transport, exceptions, and config are untouched. The snapshot test (tests/Bin/GeneratorSnapshotTest.php) fails if anyone hand-edits a generated file.

Tests

vendor/bin/pest

Contributing

  • Specs live in docs/superpowers/specs/
  • Plans live in docs/superpowers/plans/

License

MIT

This page is the package README, rendered.Edit on GitHub ↗