<?php
namespace App\Controller;
use App\Entity;
use App\Model\Calculator\DimensionsCalculator;
use App\Model\Catapulto\Encoder;
use App\Model\Catapulto\ErrorCollector;
use App\Repository\Catapulto\DeliveryOperatorsRepository;
use App\Services;
use Doctrine\ORM\EntityManagerInterface;
use Ipol\Catapulto\Catapulto\CatapultoApplication;
use Ipol\Catapulto\Catapulto\ErrorResponseException;
use Ipol\Catapulto\Core\Order\Address;
use Ipol\Catapulto\Core\Order\Item;
use Ipol\Catapulto\Core\Order\ItemCollection;
use Ipol\Catapulto\Core\Order\Order;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Uid\Uuid;
/**
* Class WidgetController
* @package App\Controller
*/
class WidgetController extends BaseController
{
/** @var CatapultoApplication */
private CatapultoApplication $catapulto;
/** @var Entity\Shop */
private Entity\Shop $shop;
/** @var RouterInterface */
protected $router;
/** @var EntityManagerInterface */
protected $em;
/** @var array Заголовки ответа */
const RESPONSE_HEADERS = [
'Access-Control-Allow-Headers' => 'Accept, Accept-Language, Content-Language, Content-Type',
'Access-Control-Allow-Origin' => '*',
];
/** @var array Заголовки ответа */
const RESPONSE_HEADERS_JS = [
'Content-Type' => 'text/javascript; charset=UTF-8',
'Access-Control-Allow-Origin' => '*',
'Cache-Control' => 'no-cache'
];
public function __construct(RouterInterface $router, EntityManagerInterface $entityManager)
{
$this->router = $router;
$this->em = $entityManager;
}
/**
* @Route(path="/app/js/{insalesId}/script.js", name="app.js", schemes={"https"})
* @ParamConverter(name="shop", options={"mapping" : {"insalesId" = "insalesId"}, "repository_method" = "getByInsalesIdEx"})
* @param Entity\Shop $shop
* @return Response
*/
public function getAppJs(Entity\Shop $shop)
{
$widgetParams = $this->getWidgetParams($shop);
$markupOperators = [];
$allOperators = $shop->getCalculation()->getCalculationDeliveryOperators()->toArray();
foreach ($allOperators as $operator) {
$markupOperators[] = [
'id'=>$operator->getDeliveryOperator()->getOperatorId(),
'mktp'=>$operator->getMarkupType(),
'mkvl'=>$operator->getMarkupVal()
];
}
$js = $this->render(
'js/script.js.twig',
[
'shop' => $shop,
'widgetParams' => $widgetParams,
'operatorsMarkups' => $markupOperators,
'defaultMarkup' => [
'tp'=>$shop->getCalculation()->getDefaultMarkupType(),
'vl'=>$shop->getCalculation()->getDefaultMarkupVal()
],
'addDadataScript' => $shop->getCalculation()->getEnableDadataInInsales()?'true':'false',
'free_delivery_from' => $shop->getCalculation()->getFreeDeliveryFrom(),
]
);
return new Response($js->getContent(), Response::HTTP_OK, self::RESPONSE_HEADERS_JS);
}
/**
* @Route(path="/widget/ajax/{insalesId}/cargo/", name="getCargo", schemes={"https"})
* @ParamConverter(name="shop", options={"mapping" : {"insalesId" = "insalesId"}, "repository_method" = "getByInsalesIdEx"})
* @param Request $request
* @param Entity\Shop $shop
* @return Response
*/
public function getCargo(Request $request, Entity\Shop $shop)
{
if ($request->getMethod() != $request::METHOD_POST) {
return new Response(null, Response::HTTP_OK);
}
/** @var array $fields */
$fields = \json_decode($request->getContent(), true);
if (!$shop instanceof Entity\Shop)
return new Response(null, Response::HTTP_OK);
$order = (new Entity\Order())->setShop($shop)->setProducts($fields['order_lines']);
$cargoComment = '';
foreach ($fields['order_lines'] as $item) {
$cargoComment .= $item['title'] . '(' . $item['quantity'] . ');';
}
// габариты в см и кг для виджета
$result = [
'width' => $order->getWidth(),
'length' => $order->getLength(),
'height' => $order->getHeight(),
'quantity' => 1, // always 1
'weight' => $order->getWeight(),
'cargo_comment' => $cargoComment
];
return $this->json($result, Response::HTTP_OK, self::RESPONSE_HEADERS);
}
/**
* @Route (path="/widget/ajax/{insalesId}/", name="widget")
* @ParamConverter(name="shop", options={"mapping" : {"insalesId" = "insalesId"}, "repository_method" = "getByInsalesIdEx"})
* @param Services\Insales\User $user
* @param Entity\Shop $shop
* @param Request $request
* @param Encoder $encoder
* @return Response
*/
public function widget(Services\Insales\User $user, Entity\Shop $shop, Request $request, Encoder $encoder)
{
if (!$request->request->get('METHOD') || !$request->request->get('PARAMS')) {
return $this->json(['error' => 'METHOD_PARAMS_NOT_FOUND'], Response::HTTP_OK, self::RESPONSE_HEADERS);
}
$this->catapulto = new CatapultoApplication(
$shop->getConnection()->getApikey(),
$shop->getConnection()->getCustomBaseApiUrl(),
$shop->getConnection()->getConnectionTimeout() ?? 6,
$encoder
);
$this->shop = $shop;
$params = json_decode($request->request->get('PARAMS'), true);
// Проброс между методами
$method = explode('_', $request->request->get('METHOD'));
$method = implode(array_map('ucfirst', $method));
$method = 'widget' . $method . 'Response';
if (method_exists(__CLASS__, $method)) {
$response = $this->{$method}($params);
} else {
$response = $this->json(['error' => 'METHOD_NOT_EXISTS'], Response::HTTP_OK, self::RESPONSE_HEADERS);
}
return $response;
}
/**
* @param array $data
*
* @return Response
*/
protected function widgetCreateRateResponse(array $data): Response
{
if (empty($data['location']['term']) || empty($data['location']['iso'])) {
return $this->json(['error' => 'GEO_DATA_EMPTY'], Response::HTTP_OK, self::RESPONSE_HEADERS);
}
if (!isset($data['pickup_days_shift'])) $data['pickup_days_shift'] = 0;
$cOrder = new Order();
// контакт отправителя
$contactSender = $this->shop->getCalculation()->getSenderId();
if ((int)$contactSender === 0) {
return $this->json(['error' => 'EMPTY_SENDER_ID'], Response::HTTP_OK, self::RESPONSE_HEADERS);
}
// отправитель из настроек модуля
$sender = new Address();
$sender->setZip($this->shop->getCalculation()->getSenderZip())
->setField('locality_id', $this->shop->getCalculation()->getSenderLocalityId());;
if (isset($data['sender_contact_data']['cityFrom'])) {
$sender->setField('cityFrom', $data['sender_contact_data']['cityFrom']);
}
$cOrder->setAddressFrom($sender);
if (empty($sender->getField('locality_id')) || empty($sender->getZip())) {
return $this->json(['error' => 'WRONG_SENDER_CONTACT_DATA'], Response::HTTP_OK, self::RESPONSE_HEADERS);
}
// получаем информацию о городе получателя
$geoData = $this->catapulto->geo($data['location']['term'], $data['location']['city_name'], 'ru', 1);
if ($geoData->isError() || $geoData->getResponse()->getGeo()->getQuantity() === 0) {
$errorCollector = new ErrorCollector($geoData);
return $this->json(['error' => 'R' . $errorCollector->getErrorCode() . ': NOT_EXISTING_ADDRESS', 'message' => $errorCollector->getErrorText()], Response::HTTP_OK, self::RESPONSE_HEADERS);
}
// получатель
$receiver = new Address();
$receiver->setField('locality_id', $geoData->getResponse()->getGeo()->getFirst()->getId())
->setZip($geoData->getResponse()->getGeo()->getFirst()->getZip())
->setField('cityFrom', $data['location']['term']);
$cOrder->setAddressTo($receiver);
// создаем контакт получателя
/*$contactData = $this->catapulto->contactCreate($cOrder);
if ($contactData->isError() || empty($contactData->getResponse()->getId())) {
$errorCollector = new ErrorCollector($contactData);
return $this->json(['error' => 'CANNOT_CREATE_CONTACT', 'message' => $errorCollector->getErrorText()], Response::HTTP_OK, self::RESPONSE_HEADERS);
}*/
// данные отгрузки
$itemCollection = new ItemCollection();
$cargoSubDataNames = ['cargo_comment', 'height', 'length', 'width', 'quantity', 'weight'];
$cargoRequestArrayData = $data['cargo_data'];
foreach ($cargoSubDataNames as $name) {
if (isset($cargoRequestArrayData[$name]) && !empty($cargoRequestArrayData[$name])) {
$item = new Item();
$item->setWidth((int)$cargoRequestArrayData['width']) // мм
->setHeight((int)$cargoRequestArrayData['height']) // мм
->setLength((int)$cargoRequestArrayData['length']) // мм
->setWeight((int)$cargoRequestArrayData['weight']) // грамм
->setQuantity((int)$cargoRequestArrayData['quantity'])
->setField('comment', $cargoRequestArrayData['cargo_comment'] ?? 'empty')
->setField('type', $data['delivery_type'] ?? 'parcel');// тип отправления ['docs', 'parcel']
$itemCollection->add($item);
}
}
$cOrder->setItems($itemCollection);
// создаем отгрузку
$cargoData = $this->catapulto->cargoCreate($cOrder);
if ($cargoData->isError() || empty($cargoData->getResponse()->getId())) {
$errorCollector = new ErrorCollector($cargoData);
return $this->json(['error' => 'R' . $errorCollector->getErrorCode() . ': CANNOT_CREATE_CARGO', 'message' => $errorCollector->getErrorText()], Response::HTTP_OK, self::RESPONSE_HEADERS);
}
$cOrder->setField('cargoes', [$cargoData->getResponse()->getId()]);
$cOrder->setField('dadata_variant', $data['dadata_selected_choice']);
$cOrder->setField('sender_contact_id', intval($contactSender));
$cOrder->setField('pickup_days_shift', intval($data['pickup_days_shift']));
// получаем иконки и доставки
$iconData = [];
$iconResponse = $this->catapulto->companyIcon();
while ($item = $iconResponse->getResponse()->getCompanies()->getNext()) {
$iconData[$item->getOperatorId()] = $item->getAllFields();
}
// создаем рассчет доставки
$rateData = $this->catapulto->rateCreate($cOrder);
if ($rateData->isError() || empty($rateData->getResponse()->getKey())) {
$errorCollector = new ErrorCollector($rateData);
return $this->json(['error' => 'R' . $errorCollector->getErrorCode() . ': CANNOT_CREATE_RATES', 'message' => $errorCollector->getErrorText()], Response::HTTP_OK, self::RESPONSE_HEADERS);
}
$result = [
'key' => $rateData->getResponse()->getKey(),
'locations' => [
'sender' => [
'locality_id' => $this->shop->getCalculation()->getSenderLocalityId(),
'zip' => $this->shop->getCalculation()->getSenderZip()
],
'contact' => [
'locality_id' => $cOrder->getAddressTo()->getField('locality_id'),
'zip' => $cOrder->getAddressTo()->getZip()
],
],
'params' => [
'sender_locality_id' => $this->shop->getCalculation()->getSenderLocalityId(),
'receiver_locality_id' => $cOrder->getAddressTo()->getField('locality_id'),
'cargoes' => [$cargoData->getResponse()->getId()]
],
'icons' => $iconData
];
return $this->json($result, Response::HTTP_OK, self::RESPONSE_HEADERS);
}
/**
* @param $data
*
* @return Response
*/
public function widgetGetRateResponse($data): Response
{
if (empty($data['rate_id']) || !is_string($data['rate_id'])) {
return $this->json(['error' => 'RATE_ID_IS_EMPTY'], Response::HTTP_OK, self::RESPONSE_HEADERS);
}
$rateId = $data['rate_id'];
$filter = [
'shipping_type_filter' => $data['shipping_type_filter'] ?: 'd2d',
'pickup_days_shift' => 0
];
if ($data['pickup_days_shift'] > 0 && $data['pickup_days_shift'] <= 365) {
$filter['pickup_days_shift'] = (int)$data['pickup_days_shift'];
}
$filter['services_filter'] = null;
if (isset($data['services_filter']) && in_array($data['services_filter'], ['NP', 'COD', 'NP,COD'])) {
$filter['services_filter'] = $data['services_filter'];
}
if (!isset($data['need_insurance'])) {
$data['need_insurance'] = false;
}
if (!isset($data['insured_value'])) {
$data['insured_value'] = 0;
}
$rate = $this->catapulto->rateRead(
$rateId,
$filter['pickup_days_shift'],
[$filter['shipping_type_filter']],
[$filter['services_filter']],
($data['need_insurance'] === true),
$data['insured_value']
);
if ($rate->isError()) {
$errorObj = new ErrorCollector($rate);
return $this->json(['error' => 'R' . $errorObj->getErrorCode() . ': CANNOT_GET_RATE', 'message' => $errorObj->getErrorText()], Response::HTTP_OK, self::RESPONSE_HEADERS);
}
$result = [
'count' => $rate->getResponse()->getCount(),
'rate_completed' => $rate->getResponse()->isRateCompleted(),
'results' => []
];
while ($item = $rate->getResponse()->getResults()->getNext()) {
/*
* Модификаторы цены
*/
$price = $item->getPrice();
// Увеличение стоимости доставки (%)
if ($this->shop->getCalculation()->getAppendDeliveryPricePercent() > 0) {
$price = $price + ($price / 100 * $this->shop->getCalculation()->getAppendDeliveryPricePercent());
}
// Увеличение стоимости доставки (фиксированная)
if ($this->shop->getCalculation()->getAppendDeliveryPriceFixed() > 0) {
$price += $this->shop->getCalculation()->getAppendDeliveryPriceFixed();
}
// Округление стоимости доставки
if ($this->shop->getCalculation()->getRoundPrice() > 0) {
$price = $this->shop->getCalculation()->roundPrice($price);
}
// Бесплатная доставка от
if ($this->shop->getCalculation()->getFreeDeliveryFrom() < floatval($data['insured_value'])//$item->getPrice()
&& $this->shop->getCalculation()->getFreeDeliveryFrom() > 0) {
$price = 0;
}
$item->setPrice((int)$price);
$result['results'][] = $item->getAllFields();
}
return $this->json($result, Response::HTTP_OK, self::RESPONSE_HEADERS);
}
/**
* @param $data
*
* @return Response
*/
public function widgetGetTerminalsResponse($data): Response
{
$terminalRequestData = $data['terminal_request_data'];
if (empty($terminalRequestData['sender_locality_id']) || empty($terminalRequestData['receiver_locality_id'])) {
return $this->json(['error' => 'EMPTY_TERMINAL_DATA'], Response::HTTP_OK, self::RESPONSE_HEADERS);
}
$cOrder = new Order();
// sender
$addressFrom = new \Ipol\Catapulto\Core\Order\Address();
$addressFrom->setField('locality_id', (int)$terminalRequestData['sender_locality_id']);
$cOrder->setAddressFrom($addressFrom);
// receiver
$addressTo = new \Ipol\Catapulto\Core\Order\Address();
$addressTo->setField('locality_id', (int)$terminalRequestData['receiver_locality_id']);
$cOrder->setAddressTo($addressTo);
// add fields
$cOrder->setField('limit', 300)
->setField('page', isset($data['page']) ? (int)$data['page'] : 1);
if ($terminalRequestData['company']) {
$cOrder->setField('company', $terminalRequestData['company']);
}
if (isset($data['services_filter']) && !empty($data['services_filter'])) {
$cOrder->setField('services_filter', $data['services_filter']);
}
if (isset($terminalRequestData['cargoes']) && is_array($terminalRequestData['cargoes']) && !empty($terminalRequestData['cargoes']))
$cOrder->setField('cargoes', $terminalRequestData['cargoes']);
$terminals = $this->catapulto->terminalList($cOrder);
if ($terminals->isError() || $terminals->getResponse()->getStatus() !== 'ok') {
$errorObj = new ErrorCollector($terminals);
return $this->json(['error' => 'R' . $errorObj->getErrorCode() . ': CANNOT_GET_TARIFF', 'message' => $errorObj->getErrorText()], Response::HTTP_OK, self::RESPONSE_HEADERS);
}
$result = [
'data' => $terminals->getResponse()->getData()->getAllFields(),
'status' => $terminals->getResponse()->getStatus()
];
foreach ($result['data']['data'] as &$point) {
$point['point-type'] = $point['point_type'];
unset($point['point_type']);
}
unset($point);
return $this->json($result, Response::HTTP_OK, self::RESPONSE_HEADERS);
}
/**
* @param $data
*
* @return Response
*/
public function widgetGetTerminalResponse($data): Response
{
if (empty($data['terminal_id'])) {
return $this->json(['error' => 'TERMINAL_ID_IS_EMPTY'], Response::HTTP_OK, self::RESPONSE_HEADERS);
}
$terminal = $this->catapulto->terminalRead((int)$data['terminal_id']);
if ($terminal->isError()) {
$errorObj = new ErrorCollector($terminal);
return $this->json(['error' => 'R' . $errorObj->getErrorCode() . ': CANNOT_GET_TERMINAL', 'message' => $errorObj->getErrorText()], Response::HTTP_OK, self::RESPONSE_HEADERS);
}
$result = [
'status' => $terminal->getResponse()->getStatus() ?? null,
'data' => []
];
while ($item = $terminal->getResponse()->getData()->getNext()) {
$point = $item->getAllFields();
$point['point-type'] = $point['point_type'];
unset($point['point_type']);
$result['data'][] = $point;
}
return $this->json($result, Response::HTTP_OK, self::RESPONSE_HEADERS);
}
/**
* @param $data
*
* @return Response
*/
public function widgetGetTariffResponse($data): Response
{
if (empty($data['tariff_id'])) {
return $this->json(['error' => 'TARIFF_ID_IS_EMPTY'], Response::HTTP_OK, self::RESPONSE_HEADERS);
}
$tariffId = $data['tariff_id'];
$filter = [
'pickup_days_shift' => 0,
];
if ($data['pickup_days_shift'] > 0 && $data['pickup_days_shift'] <= 365) {
$filter['pickup_days_shift'] = (int)$data['pickup_days_shift'];
}
$tariff = $this->catapulto->tariffRead($tariffId, $filter['pickup_days_shift']);
if ($tariff->isError()) {
$errorObj = new ErrorCollector($tariff);
return $this->json(['error' => 'R' . $errorObj->getErrorCode() . ': CANNOT_GET_TARIFF', 'message' => $errorObj->getErrorText()], Response::HTTP_OK, self::RESPONSE_HEADERS);
}
$result = [
[
'id' => $tariff->getResponse()->getId(),
'time-slots' => $tariff->getResponse()->getTimeSlots()
]
];
return $this->json($result, Response::HTTP_OK, self::RESPONSE_HEADERS);
}
/**
* Возвращает массив настроек для подключения виджета Catapulto
*
* @return array
*/
public function getWidgetParams(Entity\Shop $shop)
{
$defaultSendType = $shop->getCalculation()->getWidgetDeliveryFrom();
$invTKSendTypes = [];
$tkSTypes = $shop->getCalculation()->getCalculationDeliveryOperators();
foreach ($tkSTypes as $tk) {
if (empty($tk->getSendType())) continue;
if ($tk->getSendType() === $defaultSendType) continue;
$invTKSendTypes[] = $tk->getDeliveryOperator()->getOperatorId();
}
$arParams = [
'popup_mode' => true,
//'default_city' => self::$city,
'location' => [
//'city' => self::$city,
'address' => '',
],
'service_path' => $this->router->generate('widget',
[
'insalesId' => $shop->getInsalesId()
],
UrlGeneratorInterface::ABSOLUTE_URL
),
'cargo' => [], // TODO габариты по умолчанию
'sender_contact_params' => [
'locality_id' => $shop->getCalculation()->getSenderLocalityId(),
'zip' => $shop->getCalculation()->getSenderZip(),
'cityFrom' => $shop->getCalculation()->getSenderCity()
],
'dadata_token' => $shop->getCalculation()->getDadataApikey(),
'only_delivery_type' => $this->getParamDeliveryType($shop),
'services_filter' => '',
'startTabMap' => $shop->getCalculation()->getMapOpenMode() === 'map',
'delivery_type' => $shop->getCalculation()->getWidgetDeliveryFrom(),
'need_insurance' => $shop->getCalculation()->getMindEnsurance(),
'insured_value' => '', // TODO стоимость запрашивается и устанавливается через JS
'day_shift' => (int)$shop->getCalculation()->getAppendDeliveryDay(),
'operators' => $this->prepareOperatorsName(),
'paysystems' => $this->getPaymentCorresponds($shop),
'inverse_delivery_type_for_operators' => $invTKSendTypes,
];
if ($shop->getConnection()->isTestMode()) {
$customWSSUrl = $shop->getConnection()->getCustomBaseWsUrl();
if (!empty($customWSSUrl)) $arParams['ws_api_domain'] = $customWSSUrl;
}
return $arParams;
}
/**
* @param Entity\Shop $shop
*
* @return string|null
*/
public function getParamDeliveryType(Entity\Shop $shop)
{
$deliveryType = $shop->getCalculation()->getWidgetDeliveryTypes();
return $deliveryType == 'All' ? '' : $deliveryType;
}
/**
* Подготавливает информацию о операторах в виде массива [operator_id => operator_display]
* @return array
*/
private function prepareOperatorsName()
{
$deliveryOperatorsRepository = $this->em->getRepository(Entity\Catapulto\DeliveryOperators::class);
$arOperators = $deliveryOperatorsRepository->findAll();
$result = [];
array_map(function ($val) use (&$result) {
/** @var Entity\Catapulto\DeliveryOperators $val */
$result[$val->getOperatorId()] = $val->getOperatorDisplay();
}, $arOperators);
return $result;
}
/**
* Маппинг платежных систем
* @param Entity\Shop $shop
*
* @return array
*/
public function getPaymentCorresponds(Entity\Shop $shop)
{
$cash = $shop->getCalculation()->getCash() ?? [];
$card = $shop->getCalculation()->getCard() ?? [];
$arPayments = [];
foreach ($cash as $ps) $arPayments[$ps] = "CASH";
foreach ($card as $ps) {
if (!empty($arPayments[$ps])) $arPayments[$ps] .= ",CARD";
else $arPayments[$ps] = "CARD";
}
return $arPayments;
}
/**
* Получение WS токена для виджета
* @return \Symfony\Component\HttpFoundation\JsonResponse
*/
public function widgetGetWsResponse()
{
$token = $this->catapulto->getWSToken();
if ($token->isError()) {
$errorObj = new ErrorCollector($token);
return $this->json(['error' => 'R' . $errorObj->getErrorCode() . ': CANNOT_GET_TERMINAL', 'message' => $errorObj->getErrorText()], Response::HTTP_OK, self::RESPONSE_HEADERS);
}
$token = $token->getResponse()->getWsToken();
return $this->json((['ws_token'=>$token] ?? []), Response::HTTP_OK, self::RESPONSE_HEADERS);
}
}