/cdek-sdk

Библиотека на PHP для интеграции с программным комплексом СДЭК

Primary LanguagePHPMIT LicenseMIT

PHP SDK для API СДЭК

Latest Stable Version Build Status Coverage Status Telegram Chat

Перед вами полное SDK для интеграции с программным комплексом СДЭК.

Возможности:

Работа с большинством методов API возможна только при наличии договора со СДЭК.

🔓 Методы, отмеченные значком слева, доступны без договора в ограниченном объеме (рассчитываются только публичные тарифы, без скидок, без тарифов для ИМ).
🔐 Методы, отмеченные таким знаком, недоступны с тестовой учетной записью.

Установка

composer require sanmai/cdek-sdk

Требования — минимальны. Нужен PHP 7.0 или выше. Работа протестирована под PHP 7.0, 7.1, 7.2.

Гарантии обратной совместимости

При разработке этой библиотеки большое внимание уделяется обратной совместимости API в пределах основной версии. Если вы установили когда-то версию ветки 0.6, например 0.6.7, то после обновления до 0.6.8 или даже до 0.6.12 вы можете рассчитывать что весь ваш код будет работать точно так же как раньше, без необходимости что-то менять, при условии, конечно, что API самих СДЭК не поменялось. Такого же принципа работы с версиями по умолчанию придерживается Composer.

Гарантии обратной совместимости в части возвращаемых типов распостраняются только на имплементируемые ими интерфейсы. Если вы получали объект имплементирующий Psr\Http\Message\ResponseInterface, то такой же объёкт вы продолжите получать. Если у возвращенного объёкта был какой-то метод, то такой же метод будет у объекта в следующей неосновной версии. Конкретный тип может быть другим, рассчитывать на это не нужно, проверять принадлежность конкретному типу также не следует. Как проверять ответы на ошибки.

Такие строгие гарантии обратной совместимости API были бы невозможны без идущей рука об руку с ними минимизации точек для расширения API: наследование для большинства классов не только не предусмотрено, но и просто невозможно. Впрочем, для удобства композиции есть необходимые интерфейсы. Мы исходим из того что добавить ещё интерфейсы проблемы не представляет, новые интерфейсы не ломают обратную совместимость.

После выхода версии 1.0 обратная совместимость будет поддерживаться в пределах мажорной версии.

Инициализация

require_once 'vendor/autoload.php';

$client = new \CdekSDK\CdekClient('account', 'password');

Реквизиты доступа следует запросить у СДЭК. Обычные логин и пароль не подходят. Если авторизация не нужна, логин и пароль можно указать пустые или пропустить вовсе.

Далее для всей работы с API используются методы объёкта $client, который мы получили выше.

Для подготовки запросов и ответов используются аннотации из Doctrine. Если вы не знаете что это, то ничего не нужно делать. Иначе обратите внимание на замечания к совместному использованию AnnotationRegistry.

Использование

Перечень основных методов класса CdekClient ниже.

Задача Метод Аргумент
Удаление заказа sendDeleteRequest DeleteRequest
Получение списка ПВЗ sendPvzListRequest PvzListRequest
Список субъектов РФ sendRegionsRequest RegionsRequest
Список городов sendCitiesRequest CitiesRequest
Регистрация заказа от ИМ sendDeliveryRequest DeliveryRequest
Регистрация заказа на доставку sendAddDeliveryRequest AddDeliveryRequest
Изменение заказа sendUpdateRequest UpdateRequest
Регистрация результата прозвона sendScheduleRequest ScheduleRequest
Вызов курьера sendCallCourierRequest CallCourierRequest
Создание преалерта sendPreAlertRequest PreAlertRequest
Отчет "Информация по заказам" sendInfoReportRequest InfoReportRequest
Расчёт стоимости доставки sendCalculationRequest CalculationRequest
Отчет "Статусы заказов" sendStatusReportRequest StatusReportRequest
Печать квитанции к заказу sendPrintReceiptsRequest PrintReceiptsRequest
Печать ШК-мест sendPrintLabelsRequest PrintLabelsRequest

Обработка ошибок

Все возвращаемые ответы содержат методы для проверки на ошибку, также для получения списка сообщений включая сообщения об ошибках.

/** @var \CdekSDK\Contracts\Response $response */
$response = $client->sendSomeRequest($request);

if ($response->hasErrors()) {
    // Обрабатываем ошибки
    foreach ($response->getMessages() as $message) {
        if ($message->getErrorCode() !== '') {
            // Это ошибка
            $message->getMessage();
        }
    }
}

В редких случаях при запросе могут возникнуть исключения. Это недоразумение будет исправлено в следующих версиях.

Получение списка ПВЗ

use CdekSDK\Requests;

$request = new Requests\PvzListRequest();
$request->setCityId(250);
$request->setType(PvzListRequest::TYPE_ALL);
$request->setCashless(true);
$request->setCodAllowed(true);
$request->setDressingRoom(true);

$response = $client->sendPvzListRequest($request);

if ($response->hasErrors()) {
    // обработка ошибок
}

/** @var \CdekSDK\Responses\PvzListResponse $response */
foreach ($response as $item) {
    /** @var \CdekSDK\Common\Pvz $item */
    // всевозможные параметры соответствуют полям из API СДЭК
    $item->Code;
    $item->Name;
    $item->Address;

    foreach ($item->OfficeImages as $image) {
        $image->getUrl();
    }
}

Расчёт стоимости доставки

use CdekSDK\Requests;

// для выполнения авторизованного запроса используется
// $request = Requests\CalculationRequest::withAuthorization();
// $request->set...() и так далее

$request = new Requests\CalculationRequest();
$request->setSenderCityPostCode('295000')
    ->setReceiverCityPostCode('652632')
    ->setTariffId(1)
    ->addPackage([
        'weight' => 0.2,
        'length' => 25,
        'width'  => 15,
        'height' => 10,
    ]);

$response = $client->sendCalculationRequest($request);

if ($response->hasErrors()) {
    // обработка ошибок
}

/** @var \CdekSDK\Responses\CalculationResponse $response */
$response->getPrice();
// double(1250)

Список регионов/субъектов РФ

use CdekSDK\Requests;

$request = new Requests\RegionsRequest();
$request->setPage(0)->setSize(10);

$response = $client->sendRegionsRequest($request);

if ($response->hasErrors()) {
    // обработка ошибок
}

foreach ($response as $region) {
    /** @var \CdekSDK\Common\Region $region */
    $region->getUuid();
    $region->getName();
    $region->getPrefix();
    $region->getCode();
    $region->getCodeExt();
    $region->getFiasGuid();
    $region->getCountryName();
    $region->getCountryCode();
    $region->getCountryCodeExt();
}

Список городов

use CdekSDK\Requests;

$request = new Requests\CitiesRequest();
$request->setPage(0)->setSize(10)->setRegionCode(50);

$response = $client->sendCitiesRequest($request);

if ($response->hasErrors()) {
    // обработка ошибок
}

foreach ($response as $location) {
    /** @var \CdekSDK\Common\Location $location */
    $location->getCityName();
    $location->getCityCode();
    $location->getCityUuid();
    $location->getCountry();
    $location->getCountryCode();
    $location->getRegion();
    $location->getRegionCode();
    $location->getRegionCodeExt();
    $location->getSubRegion();
    $location->getPaymentLimit();
    $location->getLatitude();
    $location->getLongitude();
    $location->getKladr();
    $location->getFiasGuid();
}

Регистрация заказа от интернет-магазина

Названия полей соответствуют названиям полей в официальной документации.

use CdekSDK\Common;
use CdekSDK\Requests;

$order = new Common\Order([
    'Number'   => 'TEST-123456',
    'SendCity' => Common\City::create([
        'Code' => 44, // Москва
    ]),
    'RecCity' => Common\City::create([
        'PostCode' => '630001', // Новосибирск
    ]),
    'RecipientName'  => 'Иван Петров',
    'RecipientEmail' => 'petrov@test.ru',
    'Phone'          => '+7 (383) 202-22-50',
    'TariffTypeCode' => 139, // Посылка дверь-дверь от ИМ
]);

$order->setAddress(Common\Address::create([
    'Street' => 'Холодильная улица',
    'House'  => '16',
    'Flat'   => '22',
]));

$package = Common\Package::create([
    'Number'  => 'TEST-123456',
    'BarCode' => 'TEST-123456',
    'Weight'  => 500, // Общий вес (в граммах)
    'SizeA'   => 10, // Длина (в сантиметрах), в пределах от 1 до 1500
    'SizeB'   => 10,
    'SizeC'   => 10,
]);

$package->addItem(new Common\Item([
    'WareKey' => 'NN0001', // Идентификатор/артикул товара/вложения
    'Cost'    => 500, // Объявленная стоимость товара (за единицу товара)
    'Payment' => 0, // Оплата за товар при получении (за единицу товара)
    'Weight'  => 120, // Вес (за единицу товара, в граммах)
    'Amount'  => 2, // Количество единиц одноименного товара (в штуках)
    'Comment' => 'Test item',
]));

$order->addPackage($package);

$request = new Requests\DeliveryRequest([
    'Number' => 'TESTING123',
]);
$request->addOrder($order);

$response = $client->sendDeliveryRequest($request);

if ($response->hasErrors()) {
    // обработка ошибок
}

foreach ($response->getOrders() as $order) {
    // сверяем данные заказа, записываем номер
    $order->getNumber();
    $order->getDispatchNumber();
    break;
}

Регистрация заказа на доставку

Отличается необходимость указывать тип клиента, адрес забора груза. Без необходимости указывать состав посылок, но с указанием описания вложения.

use CdekSDK\Common;
use CdekSDK\Requests;

$order = new Common\Order([
    'ClientSide' => Common\Order::CLIENT_SIDE_SENDER,
    'Number'     => 'TEST-123456',
    'SendCity'   => Common\City::create([
        'Code' => 44, // Москва
    ]),
    'RecCity' => Common\City::create([
        'PostCode' => '630001', // Новосибирск
    ]),
    'RecipientName'    => 'Иван Петров',
    'RecipientEmail'   => 'petrov@test.ru',
    'Phone'            => '+7 (383) 202-22-50',
    'TariffTypeCode'   => 1,
    'RecipientCompany' => 'Петров и партнёры, ООО',
    'Comment'          => 'Это тестовый заказ',
]);

$order->setSender(Common\Sender::create([
    'Company' => 'ЗАО «Рога и Копыта»',
    'Name'    => 'Петр Иванов',
    'Phone'   => '+7 (283) 101-11-20',
])->setAddress(Common\Address::create([
    'Street' => 'Морозильная улица',
    'House'  => '2',
    'Flat'   => '101',
])));

$order->setAddress(Common\Address::create([
    'Street'  => 'Холодильная улица',
    'House'   => '16',
    'Flat'    => '22',
]));

$package = Common\Package::create([
    'Number'  => 'TEST-123456',
    'BarCode' => 'TEST-123456',
    'Weight'  => 500, // Общий вес (в граммах)
    'SizeA'   => 10, // Длина (в сантиметрах), в пределах от 1 до 1500
    'SizeB'   => 10,
    'SizeC'   => 10,
    'Comment' => 'Обязательное описание вложения',
]);

$order->addPackage($package);

$order->addService(Common\AdditionalService::create(Common\AdditionalService::SERVICE_DELIVERY_TO_DOOR));

$request = new Requests\AddDeliveryRequest([
    'Number'          => 'TESTING123',
    'ForeignDelivery' => false,
    'Currency'        => 'RUB',
]);
$request->addOrder($order);

$response = $client->sendAddDeliveryRequest($request);

if ($response->hasErrors()) {
    // обработка ошибок
}

foreach ($response->getOrders() as $order) {
    // сверяем данные заказа, записываем номер
    $order->getNumber();
    $order->getDispatchNumber();
}

Печать квитанции к заказу

Для подготовки документов необходимо указывать или номер заказа СДЭК, DispatchNumber, или номер заказа ИМ и дату через объёкт заказа.

use CdekSDK\Common;
use CdekSDK\Requests;

$request = new Requests\PrintReceiptsRequest([
    'CopyCount' => 4,
]);
$request->addOrder(Common\Order::withDispatchNumber($dispatchNumber));

$response = $client->sendPrintReceiptsRequest($request);

if ($response->hasErrors()) {
    // обработка ошибок
}

// Или возвращаем содержимое PDF файла...
return (string) $response->getBody();

Также можно указывать в запросе сами объекты заказов, полученные из других методов. Или же можно создать заказ прямо на месте, имея известные Number и Date:

$request = new Requests\PrintReceiptsRequest();
$request->addOrder($orderFromAnotherResponse);
$request->addOrder(Common\Order::withNumberAndDate($number, new \DateTime($dateString)));

Печать ШК-мест

Печать ШК-мест производится по такому же алгоритму что и печать квитанций.

use CdekSDK\Common;
use CdekSDK\Requests;

$request = new Requests\PrintLabelsRequest([
    'PrintFormat' => Requests\PrintLabelsRequest::PRINT_FORMAT_A5,
]);
$request->addOrder(Common\Order::withDispatchNumber($dispatchNumber));

$response = $client->sendPrintLabelsRequest($request);

if ($response->hasErrors()) {
    // обработка ошибок
}

// Или возвращаем содержимое PDF файла...
return (string) $response->getBody();

Удаление заказа

use CdekSDK\Common;
use CdekSDK\Requests;

$request = Requests\DeleteRequest::create([
    'Number' => 'TESTING123',
])->addOrder(new Common\Order([
    'Number' => 'TEST-123456',
]));

$response = $client->sendDeleteRequest($request);

if ($response->hasErrors()) {
    // обработка ошибок
}

foreach ($response->getOrders() as $order) {
    // проверяем номера удалённых заказов
    $order->getNumber(); // должно быть 'TEST-123456'
}

Изменение заказа

use CdekSDK\Common;
use CdekSDK\Requests;

$request = Requests\UpdateRequest::create([
    'Number' => 'TESTING123',
])->addOrder(new Common\Order([
    'Number' => 'TEST-123456',
]));

$response = $client->sendUpdateRequest($request);

if ($response->hasErrors()) {
    // обработка ошибок
}

foreach ($response->getOrders() as $order) {
    // проверяем номера изменённых заказов
    $order->getNumber(); // должно быть 'TEST-123456'
}

Вызов курьера

use CdekSDK\Common;
use CdekSDK\Requests;

$request = Requests\CallCourierRequest::create()->addCall(Common\CallCourier::create([
    'Date'           => new \DateTime('tomorrow'),
    'DispatchNumber' => $dispatchNumber,
    'TimeBeg'        => new \DateTime('10:00'),
    'TimeEnd'        => new \DateTime('17:00'),
    'SendCityCode'   => 44,
    'SenderName'     => 'Проверка Тестович',
    'SendPhone'      => '+78001001010',
])->setAddress(Common\Address::create([
    'Street' => 'Тестовая',
    'House'  => '8',
    'Flat'   => '32',
])));

$response = $client->sendCallCourierRequest($request);

if ($response->hasErrors()) {
    // обработка ошибок
}

// Получаем номера заявок
foreach ($response->getNumbers() as $number) {
    $number; // ...
}

Регистрация информации о результате прозвона

use CdekSDK\Common;
use CdekSDK\Requests;

$request = new Requests\ScheduleRequest();
$request = $request->addOrder(Common\Order::create([
    'DispatchNumber' => '123456',
])->addAttempt(Common\Attempt::create([
    'ID'   => 500,
    'Date' => new \DateTime('next Monday'),
])->addPackage(Common\Package::create([
    'Number'  => 'TEST-123456',
    'BarCode' => 'TEST-123456',
    'Weight'  => 500,
])->addItem(new Common\Item([
    'WareKey' => 'NN0001',
    'Cost'    => 500,
    'Payment' => 0,
    'Weight'  => 120,
    'Amount'  => 2,
    'Comment' => 'Test item',
])))));

$response = $client->sendScheduleRequest($request);

if ($response->hasErrors()) {
    // обработка ошибок
}

Создание преалерта

use CdekSDK\Common;
use CdekSDK\Requests;

$request = new Requests\PreAlertRequest([
    'PvzCode'            => 'NSK333',
    'PlannedMeetingDate' => new \DateTime('2017-10-12'),
]);

$request->addOrder(Common\Order::withDispatchNumber('12345678'));

$response = $client->sendPreAlertRequest($request);

if ($response->hasErrors()) {
    // обработка ошибок
}

Трекинг

Он же отчет "Статусы заказов", используется для получения отчета по статусам заказов, включая историю изменения статусов.

use CdekSDK\Common;
use CdekSDK\Requests;

$request = new Requests\StatusReportRequest();
// можно указывать или всё сразу, или только диапазоны дат, или только конкретные заказы
$request->setChangePeriod(new Common\ChangePeriod(new \DateTime('-1 day'), new \DateTime('+1 day')));
$request->addOrder(Common\Order::withDispatchNumber($dispatchNumber));

// попросим показать историю изменения статусов заказов
$request->setShowHistory();

$response = $client->sendStatusReportRequest($request);

if ($response->hasErrors()) {
    // обработка ошибок
}

foreach ($response as $order) {
    $order->getActNumber();
    $order->getNumber();
    $order->getDispatchNumber();
    $order->getDeliveryDate();
    $order->getRecipientName();

    if ($status = $order->getStatus()) {
        $status->getDescription();
        $status->getDate();
        $status->getCode();
        $status->getCityCode();
        $status->getCityName();
    }

    $order->getReason()->getCode();
    $order->getReason()->getDescription();
    $order->getReason()->getDate();

    $order->getDelayReason()->getCode();
    $order->getDelayReason()->getDescription();
    $order->getDelayReason()->getDate();
}

Отчет "Информация по заказам"

Отчет используется для получения детальной информации по заказам.

use CdekSDK\Common;
use CdekSDK\Requests;

$request = new Requests\InfoReportRequest();
$request->setChangePeriod(new Common\ChangePeriod(new \DateTime('-1 day'), new \DateTime('+1 day')));
// можно искать только по номерам, без дат
$request->addOrder(Common\Order::withDispatchNumber($dispatchNumber));

$response = $client->sendInfoReportRequest($request);

if ($response->hasErrors()) {
    // обработка ошибок
}

foreach ($response as $order) {
    /** @var \CdekSDK\Common\Order $order */
    $order->getNumber();
    $order->getSenderCity()->getName();
    $order->getRecipientCity()->getName();

    foreach ($order->getPackages() as $package) {
        $package->getBarCode();
        $package->getVolumeWeight();
    }

    foreach ($order->getAdditionalServices() as $service) {
        $service->getServiceCode();
        $service->getSum();
    }
}

Замена базового URL интерфейса

Перечень возможных URL в документации.

$account = getenv('CDEK_ACCOUNT');
$password = getenv('CDEK_PASSWORD');
$baseUri = getenv('CDEK_BASE_URL');
// Например, это может быть https://integration.cdek-asia.cn

$client = new \CdekSDK\CdekClient($account, $password, new \GuzzleHttp\Client([
    'base_uri' => $baseUri,
]));

Сервис-провайдер для Laravel 5.1+

// config/app.php

    'providers' => [
        // ...

        \CdekSDK\LaravelCdekServiceProvider::class

        // ...
    ]

// config/services.php

    'cdek' => [
        'account'  => env('CDEK_ACCOUNT', ''),
        'password' => env('CDEK_PASSWORD', ''),
        'guzzle_options' => [ // необязательные параметры
            'base_uri' => 'https://integration.cdek-asia.cn',
            'timeout'  => 5,
        ],
    ],

Отладка получаемых ответов

Посмотреть, что конкретно отвечает СДЭК на наши запросы и какие запросы мы посылаем сами можно используя стандартный PSR-3 логгер, так, как, например, Monolog.

$client->setLogger($monolog);

Текстовые запросы и ответы в исходном виде идут с уровнем DEBUG.

Замечания

О форматах даты и времени

Для указания даты и времени в запросах везде можно использовать ровно как DateTime, так и DateTimeImmutable.

AnnotationRegistry

Если вы не используете AnnotationRegistry где-то ещё, то никакой дополнительной настройки делать не требуется.

Если же вы используете AnnotationRegistry и в ней не настроен обычный автозагрузчик классов, то его следует подключить где-то до создания CdekClient следующим образом:

\Doctrine\Common\Annotations\AnnotationRegistry::registerLoader('class_exists');

Если же нежелательно использовать обычный загрузчик классов, то можно отключить его автоматическую настройку:

\CdekSDK\Serialization\Serializer::doNotConfigureAnnotationRegistry();

Обычно ничего этого делать не нужно, всё должно работать и так.

Авторы и ссылки

Эта библиотека - хард-форк библиотеки appwilio/cdek-sdk с поддержкой более старых версий PHP и расширенной поддержкой API. Обратная совместимость с исходной библиотекой не гарантируется, но фичи и исправления будут переноситься оттуда сюда по мере возможности. Если что-то пропустили, дайте знать.

Авторы-создатели исходной библиотеки: JhaoDa и greabock.

Лицензия

Данный SDK распространяется под лицензией MIT.

This project is licensed under the terms of the MIT license.

Maintainability Codacy Badge FOSSA Status