src/Diplix/KMGBundle/Service/OrderHandler.php line 86

Open in your IDE?
  1. <?php
  2. namespace Diplix\KMGBundle\Service;
  3. use Diplix\Commons\DataHandlingBundle\Entity\SysLogEntry;
  4. use Diplix\Commons\DataHandlingBundle\Repository\SysLogRepository;
  5. use Diplix\KMGBundle\Controller\Service\Api2Controller;
  6. use Diplix\KMGBundle\Entity\Accounting\Billing;
  7. use Diplix\KMGBundle\Entity\Accounting\CoopMember;
  8. use Diplix\KMGBundle\Entity\Accounting\Job;
  9. use Diplix\KMGBundle\Entity\Address;
  10. use Diplix\KMGBundle\Entity\Availability;
  11. use Diplix\KMGBundle\Entity\Customer;
  12. use Diplix\KMGBundle\Entity\Order;
  13. use Diplix\KMGBundle\Entity\OrderStatus;
  14. use Diplix\KMGBundle\Entity\PaymentType;
  15. use Diplix\KMGBundle\Entity\Role;
  16. use Diplix\KMGBundle\Entity\User;
  17. use Diplix\KMGBundle\Exception\OrderValidationException;
  18. use Diplix\KMGBundle\Helper\ClientConfigProvider;
  19. use Diplix\KMGBundle\PdfGeneration\JobPdf;
  20. use Diplix\KMGBundle\PriceCalculator\AbstractPriceCalculator;
  21. use Diplix\KMGBundle\PriceCalculator\CalculatorService;
  22. use Diplix\KMGBundle\Repository\JobRepository;
  23. use Diplix\KMGBundle\Repository\OrderRepository;
  24. use Diplix\KMGBundle\Service\Accounting\PaymentCalculator;
  25. use Doctrine\ORM\EntityManagerInterface;
  26. use Google\Service\Monitoring\Custom;
  27. use Symfony\Component\Security\Core\Security;
  28. use Symfony\Component\Translation\TranslatorInterface;
  29. use GuzzleHttp;
  30. class OrderHandler
  31. {
  32.     /** @var EntityManagerInterface */
  33.     protected $em;
  34.     /** @var Notifier */
  35.     protected $notifier;
  36.     /** @var OrderRepository */
  37.     protected $repo;
  38.     protected $USE_TAMI true;
  39.     /** @var TaMiConnector */
  40.     protected $tami;
  41.     /** @var MailHelper */
  42.     protected $mailHelper;
  43.     /** @var Security */
  44.     protected $security;
  45.     /** @var CalculatorService */
  46.     protected $calcService;
  47.     protected $transactionTag '';
  48.     /** @var PaymentCalculator */
  49.     protected $paymentCalculator;
  50.     /** @var ClientConfigProvider  */
  51.     protected $config;
  52.     protected $tempDir;
  53.     public static $connectionSettings = array(
  54.         'connect_timeout' => "5"// abort if no response after X seconds,
  55.         'timeout' => "25"// abort if no response after X seconds,
  56.         'verify' => false// we do not care if the cert is valid or not
  57.         'http_errors' => true// throw exception if code != 200
  58.     );
  59.     public function setTransactionTag($tag)
  60.     {
  61.         $this->transactionTag $tag;
  62.         $this->notifier->setTransactionTag($tag);
  63.     }
  64.     public function getStatusObject($id)
  65.     {
  66.         return $this->em->find(OrderStatus::class,$id);
  67.     }
  68.     public function __construct(EntityManagerInterface $em,
  69.                                 Notifier $notifier,
  70.                                 TaMiConnector $taMiConnector,
  71.                                 MailHelper $mh,
  72.                                 Security $security,
  73.                                 PaymentCalculator $paymentCalculator,
  74.                                 CalculatorService $calcService,
  75.                                 ClientConfigProvider $configProvider,
  76.                                 $tempDir)
  77.     {
  78.         $this->notifier $notifier;
  79.         $this->em $em;
  80.         $this->repo $this->em->getRepository(Order::class);
  81.         $this->tami $taMiConnector;
  82.         $this->mailHelper $mh;
  83.         $this->security $security;
  84.         $this->paymentCalculator $paymentCalculator;
  85.         $this->calcService $calcService;
  86.         $this->config $configProvider;
  87.         $this->tempDir $tempDir;
  88.     }
  89.     public function enableTami($enableTami)
  90.     {
  91.         $this->USE_TAMI $enableTami;
  92.     }
  93.     protected function updateAvailabilityFromOrder(Order $order)
  94.     {
  95.         $rep $this->em->getRepository(Availability::class);
  96.         $av $rep->findOneBy(['createdFromRemoteOrderId'=>$order->getOrderId()]);
  97.         if ($order->getAssignedTo()!==null)
  98.         {
  99.             if ($av===null)
  100.             {
  101.                 $av = new Availability();
  102.                 //$av->setAvailType(Availability::KMG);
  103.                 $av->setCreatedFromRemoteOrderId($order->getOrderId());
  104.             }
  105.             if ($order->getOrderStatus()->getId()===OrderStatus::STATUS_CANCELED)
  106.             {
  107.                 $av->setAvailType(Availability::INFO);
  108.             }
  109.             else
  110.             {
  111.                 $av->setAvailType(Availability::KMG);
  112.             }
  113.             $av->setAvailFrom( clone $order->getOrderTime() );
  114.             $until = clone($av->getAvailFrom());
  115.             $until->add(new \DateInterval('PT1H'));
  116.             $av->setAvailUntil$until );
  117.             $av->setMember($order->getAssignedTo());
  118.             $av->setExtraInfo(sprintf('#%s: %s',$order->getOrderId(),$order->getOrderStatus()->getName()));
  119.         }
  120.         else
  121.         {
  122.             // remove availability for an order without an assigned member
  123.             if ($av!==null$av->setBeDeleted(true);
  124.         }
  125.         if ($av!==null)
  126.         {
  127.             $rep->persistFlush($av);
  128.         }
  129.     }
  130.     protected function syncMemberFromOrderToJobIfRequired(Order $order)
  131.     {
  132.         if ($order->getJob()!==null)
  133.         {
  134.             // if a job has already been approved - do not change the member anymore
  135.             if (!$order->getJob()->isApprovedByAccounting())
  136.             {
  137.                 $order->getJob()->setMember($order->getAssignedTo());
  138.                 if ($order->getAssignedTo()!==null)
  139.                 {
  140.                     $n $order->getAssignedTo()->getNumber();
  141.                     $order->getJob()->setRideStyle($order->getAssignedTo()->getDefaultRideStyle());
  142.                     $order->getJob()->setCarId($n);
  143.                     $order->getJob()->setDriverId($n);
  144.                 }
  145.                 else
  146.                 {
  147.                     $order->getJob()->setCarId(0);
  148.                     $order->getJob()->setDriverId(0);
  149.                 }
  150.             }
  151.             else
  152.             {
  153.                 if ($order->getAssignedTo()!==$order->getJob()->getMember())
  154.                 {
  155.                     SysLogRepository::logMessage($this->em->getConnection(),SysLogEntry::SYS_INFO,sprintf(
  156.                         'Warnung: Änderung an Order(%s):assignedTo wurde nicht in Job:member übernommen, da Job:approvedByAccounting=true war',
  157.                         $order->getOrderId()
  158.                     ));
  159.                 }
  160.             }
  161.         }
  162.     }
  163.     /**
  164.      * @param CoopMember $member
  165.      * @return GuzzleHttp\Client
  166.      * @throws GuzzleHttp\Exception\GuzzleException
  167.      * @throws \JsonException
  168.      */
  169.     public static function getClientForMemberWhoLoginsAsCustomer(CoopMember  $member): GuzzleHttp\Client
  170.     {
  171.         // check api
  172.         $client = new GuzzleHttp\Client(self::$connectionSettings);
  173.         $res $client->request("POST",$member->getXchgTargetUrl(),[
  174.             'headers'=> [
  175.                 "Content-Type"=>"application/json"
  176.             ],
  177.             'body' => json_encode([     // login as customer
  178.                 "login" => $member->getXchgLogin(),
  179.                 "password" => $member->getXchgPassword()
  180.             ], JSON_THROW_ON_ERROR)
  181.         ]);
  182.         $res json_decode($res->getBody(), true512JSON_THROW_ON_ERROR);
  183.         if ($res['success']!==true)
  184.         {
  185.             throw new \RuntimeException('Ungültige Zugangsdaten für Fremdsystem '.$member->getXchgTargetUrl());
  186.         }
  187.         if (!isset($res['customer']))
  188.         {
  189.             throw new \RuntimeException('Account ist nicht als Kunde konfiguriert im Zielsystem');
  190.         }
  191.         return $client;
  192.     }
  193.     /**
  194.      * @param Customer $customer
  195.      * @return GuzzleHttp\Client
  196.      * @throws GuzzleHttp\Exception\GuzzleException
  197.      * @throws \JsonException
  198.      */
  199.     public static function getClientForCustomerWhoLoginsAsMember(Customer  $customer): GuzzleHttp\Client
  200.     {
  201.         // check api
  202.         $client = new GuzzleHttp\Client(self::$connectionSettings);
  203.         $res $client->request("POST",$customer->getXchgPlatformUrl(),[
  204.             'headers'=> [
  205.                 "Content-Type"=>"application/json"
  206.             ],
  207.             'body' => json_encode([     // login as customer
  208.                 "login" => $customer->getXchgLoginAsMember(),
  209.                 "password" => $customer->getXchgLoginPassword()
  210.             ], JSON_THROW_ON_ERROR)
  211.         ]);
  212.         $res json_decode($res->getBody(), true512JSON_THROW_ON_ERROR);
  213.         if ($res['success']!==true)
  214.         {
  215.             throw new \RuntimeException('Ungültige Zugangsdaten für Fremdsystem '.$customer->getXchgPlatformUrl());
  216.         }
  217.         if (!isset($res['member']))
  218.         {
  219.             throw new \RuntimeException('Account ist nicht als Mitglied konfiguriert im Zielsystem');
  220.         }
  221.         return $client;
  222.     }
  223.     /**
  224.      * @param $order int|Order
  225.      * @param $member ?int
  226.      * @return Order
  227.      * @throws \Exception
  228.      */
  229.     public function assignMemberToOrder($order$member): Order
  230.     {
  231.         /** @var ?CoopMember|?int $member */
  232.         if ($member 0)
  233.         {
  234.             $member $this->em->getRepository(CoopMember::class)->findOneBy(['id'=>$member]);
  235.         }
  236.         else
  237.         {
  238.             $member null;
  239.         }
  240.         $osVermittelt $this->getStatusObject(OrderStatus::STATUS_VERMITTELT);
  241.         $osOffen $this->getStatusObject(OrderStatus::STATUS_OPEN);
  242.         $orderA $this->repo->findOneBy(['id'=>  ($order instanceof  Order) ? $order->getId() : $order  ]);
  243.         $oldMember $orderA->getAssignedTo();
  244.         $this->em->getConnection()->setAutoCommit(false);
  245.         $this->em->transactional(function($em) use($order,$osVermittelt,$osOffen,$member) {
  246.             $order $this->repo->findOneBy(['id'=>  ($order instanceof  Order) ? $order->getId() : $order  ]);
  247.             if ($order->getOrderStatus()->getId()>3)
  248.             {
  249.                 throw new \Exception('Fahrt bereits storniert oder im falschen Status. Änderung nicht mehr möglich.');
  250.             }
  251.             if ($order->getOrderStatus()->getId()==3//3 = erledigt
  252.             {
  253.                 if ($order->getOrderTime() < new \DateTime('-2 day'))
  254.                 {
  255.                     throw new \Exception('Fahrt bereits erledigt und mehr als 48h alt. Änderung nicht mehr möglich.');
  256.                 }
  257.             }
  258.             if (($order->isAssignmentConfirmed())&&($member!==null))
  259.             {
  260.                 throw new \Exception('Fahrtannahme wurde bereits bestätigt vom Mitglied. Bitte zunächst die Zuordnung entfernen sofern ein Wechsel gewünscht ist.');
  261.             }
  262.             if ( ($order->getAssignedTo()!==null)&&($member!==null) )
  263.             {
  264.                 throw new \Exception("Fahrt ist bereits einem Mitglied zugeordnet. Bitte zunächst die Zuordnung entfernen.");
  265.             }
  266.             $order->setAssignmentStatus(Order::AS_NONE);
  267.             if ($member instanceof CoopMember)
  268.             {
  269.                 if ($member->getXchgTargetUrl()!=="")
  270.                 {
  271.                     $this->sendToPlatform($order,$member);
  272.                 }
  273.                 $order->setOrderStatus$osVermittelt );
  274.                 $order->setAssignedTo($member);
  275.                 $order->setAssignmentConfirmed(false);
  276.             }
  277.             else
  278.             {
  279.                 if (($order->getAssignedTo()!==null) && ($order->getAssignedTo()->getXchgTargetUrl()!==""))
  280.                 {
  281.                     $this->cancelInPlatform($order);
  282.                     $order->setXchgTo(null);
  283.                     if ($order->getXchgStatus() !== Order::XCHG_RECEIVED_FROM_OTHER// falls Fahrt von Fremdsystem darf der Status nicht geändert werden
  284.                     {
  285.                         $order->setXchgStatus(Order::XCHG_NONE);
  286.                         $order->setXchgOrderId('');
  287.                     }
  288.                     $order->setXchgConfirmed(false);
  289.                 }
  290.                 $order->setOrderStatus($osOffen);
  291.                 $order->setAssignedTo(null);
  292.                 $order->setAssignmentConfirmed(false);
  293.             }
  294.             $this->syncMemberFromOrderToJobIfRequired($order);
  295.         });
  296.         $this->em->getConnection()->setAutoCommit(true);
  297.         // refresh order
  298.         $order $this->repo->findOneBy(['id'=>  ($order instanceof  Order) ? $order->getId() : $order  ]);
  299.         // update avail
  300.         $this->updateAvailabilityFromOrder($order);
  301.         // notify
  302.         $this->notifier->triggerOrderUpdateForMember($order,Notifier::M_CONFIRMATION_REQUIRED);
  303.         if ($oldMember!==null)
  304.         {
  305.             $this->notifier->notifyMemberAboutOrderRemoval($oldMember,$order);
  306.         }
  307.         return $order;
  308.     }
  309.     /**
  310.      * @return PaymentType
  311.      * @throws \Exception
  312.      */
  313.     protected function getDefaultPaymentType(User $userCustomer $customer null)
  314.     {
  315.         if ($customer === null$customer $user->getCustomer();
  316.         $pt $customer->getDefaultPaymentType();
  317.         if ($pt === null)
  318.         {
  319.             $pt $customer->getPaymentTypes()->get(0);
  320.             if ($pt === null) throw new \Exception("No paymentType set");
  321.         }
  322.         return $pt;
  323.     }
  324.     /**
  325.      * @param User $u
  326.      * @param Customer|null $c
  327.      * @param int $orderStatus
  328.      * @param PaymentType|int|null $paymentTypeOrId
  329.      * @param bool $autoFillOrderer
  330.      * @return Order
  331.      * @throws \Doctrine\ORM\ORMException
  332.      */
  333.     public function getNewOrder(User $uCustomer  $c null$orderStatus OrderStatus::STATUS_DRAFT$paymentTypeOrId null$autoFillOrderer=true)
  334.     {
  335.         $row = new Order();
  336.         // owner
  337.         $row->setBeOwner($u);
  338.         $row->setCustomer($c ?? $row->getBeOwner()->getCustomer());
  339.         // initial status
  340.         $row->setOrderStatus$this->getStatusObject($orderStatus));
  341.         $row->setPersonCount(1);
  342.         // start with 2 empty addresses addresses
  343.         $row->addAddress(new Address($row->getCustomer(),0)); // empty start
  344.         $row->addAddress(new Address($row->getCustomer(),1)); // empty destination
  345.         // orderer details
  346.         if ($autoFillOrderer// do not rely on the users preference in user->autofillOrdererDetails at this point
  347.         {
  348.             $row->setOrdererForename$u->getFirstName() );
  349.             $row->setOrdererName($u->getLastName());
  350.             $row->setOrdererMail($u->getEmail());
  351.             $row->setOrdererPhone($u->getPhone());
  352.         }
  353.         // payment type
  354.         if ($paymentTypeOrId!==null)
  355.         {
  356.             if ($paymentTypeOrId instanceof PaymentType)
  357.                 $row->setPaymentType$paymentTypeOrId );
  358.             else
  359.                 $row->setPaymentType$this->em->getReference(PaymentType::class,$paymentTypeOrId) );
  360.         }
  361.         else
  362.         {
  363.             $row->setPaymentType($this->getDefaultPaymentType($u,$c));
  364.         }
  365.         // default price list
  366.         $row->setPriceList(  $u->getCustomer()->getDefaultPriceList() );
  367.         // default to next day as order time
  368.         $offset $this->config->getOrderTimeOffsetOnNewOrder();
  369.         if ($offset!==null)
  370.         {
  371.             $dt = new \DateTime("now");
  372.             if ($offset 0)
  373.             {
  374.                 $dt->add(new \DateInterval(sprintf("P%dD",$offset)));
  375.             }
  376.             $row->setOrderTime($dt);
  377.         }
  378.         // PKW is default car type
  379.         $row->setCarType(Order::CARTYPE_PKW);
  380.         return $row;
  381.     }
  382.     protected function processChildOrder(Order $row)
  383.     {
  384.         if ($row->getReferencedParentOrder()!==null)
  385.         {
  386.             // the order is a child itsself.
  387.             return;
  388.         }
  389.         // actually this can only happen for BLUM orders
  390.         if (in_array($row->getDirection(), [Order::DIRECTION_TWOWAY,Order::DIRECTION_TWOWAY_REVERSE]))
  391.         {
  392.             $childRow Order::createOrUpdateChildOrder($row$row->getChildOrder());
  393.             if (is_null($childRow->getOrderStatus()))
  394.             {
  395.                 $childRow->setOrderStatus($this->em->find(OrderStatus::class, OrderStatus::STATUS_DRAFT));
  396.             }
  397.             $this->repo->persistFlush($childRow);
  398.         }
  399.         else
  400.         {
  401.             $child $row->getChildOrder();
  402.             if ($child !== null) {
  403.                 if (($this->USE_TAMI) && ($child->getRemoteStatus() !== Order::REMOTE_PENDING))
  404.                 {
  405.                     if (!$this->tami->cancelOrder($child))
  406.                     {
  407.                         SysLogRepository::logError($this->em->getConnection(),"Fehler beim Stornieren des Unterauftrags in TAMI !",$child);
  408.                     }
  409.                 }
  410.                 $child->setReferencedParentOrder(null);
  411.                 $child->setBeDeleted(true);
  412.                 $this->repo->flush($child);
  413.             }
  414.         }
  415.     }
  416.     // store order (do not initiate)
  417.     public function storeOrUpdate(Order $order)
  418.     {
  419.         /*
  420.         if ($this->em->contains($order))
  421.         {
  422.             throw new \LogicException('storeNewOrder(): Cannot be used with an existing order !');
  423.         }
  424.         */
  425.         /*
  426.         if ($order->getOrderStatus()->getId()!==OrderStatus::STATUS_DRAFT)
  427.         {
  428.             throw new \LogicException('storeNewOrder(): order is not a draft !');
  429.         }
  430.         */
  431.         // ensure that all Addresses match our customer and user
  432.         /** @var Address $a */
  433.         foreach ($order->getAddressList() as $a) {
  434.             $a->setCustomer($order->getCustomer());
  435.             $a->setBeOwner($order->getBeOwner());
  436.             $a->setOwningOrder($order);
  437.         }
  438.         // ensure that the pricelist is set and let it process the order
  439.         if ($order->getPriceList()===null)
  440.         {
  441.            throw new OrderValidationException("Preisliste nicht gesetzt.");
  442.         }
  443.         $pti $order->getPaymentType()->getId();
  444.         $pta array_map(function(PaymentType $paymentType) { return $paymentType->getId(); }, $order->getPriceList()->getPaymentTypes()->toArray());
  445.         if (!in_array($pti,$pta))
  446.         {
  447.             $ptaNames array_map(function(PaymentType $paymentType) { return $paymentType->getName(); }, $order->getPriceList()->getPaymentTypes()->toArray());
  448.             throw new OrderValidationException(sprintf("Die Preisliste ist mit der gewählten Zahlart (%s) leider nicht nutzbar. Möglich: %s",$order->getPaymentType()->getName(),implode(",",$ptaNames)));
  449.         }
  450.         $orderCreated $order->getBeCreated() ?? new \DateTime();
  451.         if ( ($order->getPriceList()->getValidUntil()!==null) && ($orderCreated->getTimestamp() > $order->getPriceList()->getValidUntil()->getTimestamp()) )
  452.         {
  453.             throw new OrderValidationException(sprintf('Preiseliste ist nicht mehr gültig für den Auftrag erzeugt am %s.',$orderCreated->format('d.m.Y')));
  454.         }
  455.         if ( ($order->getPriceList()->getValidSince()!==null) && ($orderCreated->getTimestamp() < $order->getPriceList()->getValidSince()->getTimestamp()) )
  456.         {
  457.             throw new OrderValidationException(sprintf('Preiseliste ist noch nicht gültig für einen Auftrag erzeugt am %s.',$orderCreated->format('d.m.Y')));
  458.         }
  459.         $plc AbstractPriceCalculator::getCalculator($order->getPriceList());
  460.         $plc->postProcessOrder($order);
  461.         if ($order->getLastEstimatedDistance() === 0)
  462.         {
  463.             // if the lastEstimatedDistance field is empty
  464.             // (e.g. because the order came via api or with a PL which does not require a distance)
  465.             // we try to get it here
  466.             $flatWaypoints = [ $order->getPriceList()->getHomeAddress() ]; // start/endpoint
  467.             $addresses = [];
  468.             foreach ($order->getAddressList() as $a) {
  469.                 $flatWaypoints[] = CalculatorService::addressToFlatString(json_decode(json_encode($a),true));
  470.                 $addresses[] = json_decode(json_encode($a),true);
  471.             }
  472.             try {
  473.                 $distances =  $this->calcService->getDistance($flatWaypoints);
  474.                 $order->setLastEstimatedDistance$this->calcService->getDistanceSumInKm($distances) );
  475.                 if ($order->getLastEstimatedPrice() == 0)
  476.                 {
  477.                     $pp $this->calcService->estimatePrice(
  478.                         $order->getPriceList(),
  479.                         $addresses,
  480.                         [],
  481.                         $order->getLastEstimatedDistance(),
  482.                         $order->getCustomer()
  483.                     );
  484.                     $order->setLastEstimatedPrice($pp);
  485.                 }
  486.             }
  487.             catch (\Throwable $ex)
  488.             {
  489.                 // do not crash on api error (e.g. OVER_QUERY_LIMIT)
  490.                 $order->setInternalComment($order->getInternalComment()."\n".$ex->getMessage());
  491.             }
  492.         }
  493.         $this->repo->persistFlush($order);
  494.         // deleted addresses are still in our entity
  495.         $order->removeDeletedAddressesFromList();
  496.         // todo:blum child order handling -> CHECK FUNCTIONALITY
  497.         $this->processChildOrder($order);
  498.     }
  499.     public static function transactionalWithTableLock(EntityManagerInterface $em$lockTableForWrite, callable $callback)
  500.     {
  501.         try {
  502.             // LOCK TABLES is not transaction-safe and implicitly commits any active transaction before attempting to lock the tables.
  503.             // START TRANSACTION releases existing locks
  504.             // therefore we neeed to disable autocommit instead of explicitly starting a transaction
  505.             // ROLLBACK doesn’t release table locks.
  506.             $em->getConnection()->setAutoCommit(false);
  507.             $em->getConnection()->exec('LOCK TABLES '.$lockTableForWrite.' WRITE;');
  508.             $callback($em);
  509.             $em->flush();
  510.             $em->getConnection()->commit();
  511.             $em->getConnection()->exec('UNLOCK TABLES;'); // UNLOCK TABLES implicitly commits any active transaction, but only if LOCK TABLES have been used to acquire table locks
  512.         }
  513.         catch (\Throwable $e) {
  514.             $em->close();
  515.             $em->getConnection()->rollBack();
  516.             $em->getConnection()->exec('UNLOCK TABLES;');
  517.             throw $e;
  518.         }
  519.         $em->getConnection()->setAutoCommit(true);
  520.     }
  521.     // Bestellung auslösen / falls bereits ausgelöst entsprechend aktualisieren
  522.     public function initiateOrder(Order $order$suppressMail false)
  523.     {
  524.         $newOrderCreated false;
  525.         if ($order->getOrderStatus()->getId()===OrderStatus::STATUS_DRAFT)
  526.         {
  527.             if ($order->getOrderId()!=='')
  528.             {
  529.                 throw new OrderValidationException('initiateOrder(): order already has an order id !');
  530.             }
  531.             $open $this->em->find(OrderStatus::class,OrderStatus::STATUS_OPEN);
  532.             self::transactionalWithTableLock($this->em"orders", function(EntityManagerInterface $em) use($order,$open)
  533.             {
  534.                 $newId $this->repo->getNewOrderId$order->getOrderTime() );
  535.                 $order->setOrderId($newId);
  536.                 $order->setOrderStatus($open);
  537.                 $order->setOrderInitiatedOn(new \DateTime());
  538.             });
  539. //            $this->em->transactional(function(EntityManagerInterface $em) use($order,$open) {
  540. //                $em->getConnection()->exec('LOCK TABLES orders WRITE;');
  541. //                $newId = $this->repo->getNewOrderId( $order->getOrderTime() );
  542. //                $order->setOrderId($newId);
  543. //                $order->setOrderStatus($open);
  544. //                $order->setOrderInitiatedOn(new \DateTime());
  545. //                $em->getConnection()->exec('UNLOCK TABLES;');
  546. //            });
  547.             $newOrderCreated true;
  548.         }
  549.         else
  550.         {
  551.             if ($order->getOrderId()==='')
  552.             {
  553.                 throw new OrderValidationException('initiateOrder(): order has no order id !');
  554.             }
  555.         }
  556.         // update existing tami
  557.         if ( ($this->USE_TAMI/*|| ($order->getRemoteStatus() !== Order::REMOTE_PENDING)*/ )
  558.         {
  559.             if (!$this->tami->submitOrder($order))
  560.             {
  561.                 SysLogRepository::logError($this->em->getConnection(),"Übertragung nach TaMi fehlgeschlagen: ".$order->getRemoteResult(),$order);
  562.             }
  563.         }
  564.         // create job
  565.         if ($order->getJob()===null)
  566.         {
  567.             $order->setJobJob::createForOrder($order ) );
  568.             if ($this->config->isJobAlwaysRecalculateCustomerPrice())
  569.             {
  570.                 $order->getJob()->setRecalculateCustomerPrice(true);
  571.             }
  572.         }
  573.         else
  574.         {
  575.             $order->getJob()->updateFromOrder($order);
  576.         }
  577.         $this->em->flush(); // update order
  578.         // email confirmation
  579.         if (!$suppressMail)
  580.         {
  581.             try
  582.             {
  583.                 $this->mailHelper->OrderConfirmationMail($order);
  584.             }
  585.             catch (\Throwable $ex)
  586.             {
  587.                 SysLogRepository::logError($this->em->getConnection(),"Mailzustellung bei Bestellung fehlgeschlagen: ".$ex->getMessage(),$order,0,'Mail');
  588.             }
  589.         }
  590.         // emit notification
  591.         $this->notifier->notifyAboutOrder($order$newOrderCreated);
  592.         $this->notifier->notifyTelegramAboutOrder($order,$newOrderCreated);
  593.         // update availability
  594.         $this->updateAvailabilityFromOrder($order);
  595.         // todo: child order -> move from controller to here if possible
  596.         // problem: when not doing in controller, no redirect to actual order occurs, "/new" order is created everytime on a
  597.         // subsequent error
  598. //        if ($order->getChildOrder()!==null)
  599. //        {
  600. //            $this->initiateOrder($order->getChildOrder());
  601. //        }
  602.     }
  603.     public  function isLagTooLow(\DateTime $dt)
  604.     {
  605.         return false// TODO: re-enable temporarily removed check
  606.         if ($this->security->isGranted(Role::NO_TIME_RESTRICTION))
  607.         {
  608.             return false;
  609.         }
  610.         $earliest = new \DateTime();
  611.         $earliest->add(new \DateInterval(sprintf("PT%dH"Order::TIME_LAG_HOURS)));
  612.         return ($dt $earliest);
  613.     }
  614.     public function ensureThatOrderCanBeCancelled(Order $row): void
  615.     {
  616.         if (null !== $row->getReferencedParentOrder())
  617.         {
  618.             throw new OrderValidationException("Diese Fahrt wurde automatisch angelegt und kann nicht gelöscht/storniert werden. Bitte bearbeiten Sie den Hauptauftrag !");
  619.         }
  620.         if ($this->IsLagTooLow($row->getOrderTime()))
  621.         {
  622.             throw new OrderValidationException('order.edit-not-possible-lag');
  623.         }
  624.         if (
  625.             (!$this->security->isGranted(Role::EDIT_CLOSED_ORDERS))
  626.             &&
  627.             (!in_array($row->getOrderStatus()->getId(), [OrderStatus::STATUS_OPENOrderStatus::STATUS_DRAFTOrderStatus::STATUS_VERMITTELT], true))
  628.         )
  629.         {
  630.             throw new OrderValidationException('Fahrt kann nicht mehr storniert werden.');
  631.         }
  632.         if ($row->isAssignmentConfirmed() && ($row->getAssignedTo()!==null))
  633.         {
  634.             throw new OrderValidationException('Fahrt ist bereits zugeordnet. Bitte rufen Sie die Disposition an um diesen Auftrag zu stornieren');
  635.         }
  636.         if ($row->getChildOrder()!==null)
  637.         {
  638.             if ($this->IsLagTooLow$row->getChildOrder()->getOrderTime() ))
  639.             {
  640.                 throw new OrderValidationException('Unterauftrag: order.edit-not-possible-lag');
  641.             }
  642.             if ($row->getChildOrder()->isAssignmentConfirmed() && ($row->getChildOrder()->getAssignedTo()!==null))
  643.             {
  644.                 throw new OrderValidationException('Unterauftrag ist bereits zugeordnet. Bitte rufen Sie die Disposition an um diesen Auftrag zu stornieren');
  645.             }
  646.         }
  647.     }
  648.     public function cancelOrder(Order $row)
  649.     {
  650.         $this->ensureThatOrderCanBeCancelled($row);
  651.         $storniert $this->em->find(OrderStatus::class,OrderStatus::STATUS_CANCELED);
  652.         $this->em->getConnection()->setAutoCommit(false);
  653.         $this->em->transactional(function(EntityManagerInterface $em) use($row,$storniert) {
  654.             $em->getConnection()->exec('LOCK TABLES orders WRITE;');
  655.             $row->setOrderStatus($storniert);
  656.             if ($row->getChildOrder()!==null)
  657.             {
  658.                 $row->getChildOrder()->setOrderStatus($storniert);
  659.             }
  660.             $em->getConnection()->exec('UNLOCK TABLES;');
  661.         });
  662.         $this->em->getConnection()->setAutoCommit(true);
  663.         // update existing tami
  664.         if ( ($this->USE_TAMI) && ($row->getRemoteStatus() !== Order::REMOTE_PENDING))
  665.         {
  666.             if (!$this->tami->cancelOrder($row))
  667.             {
  668.                 SysLogRepository::logError($this->em->getConnection(),"Fehler beim Stornieren des Auftrags in TAMI !",$row);
  669.             }
  670.             if ( ($row->getChildOrder()!==null) && ($this->USE_TAMI) && ($row->getChildOrder()->getRemoteStatus() !== Order::REMOTE_PENDING) )
  671.             {
  672.                 if (!$this->tami->cancelOrder($row))
  673.                 {
  674.                     SysLogRepository::logError($this->em->getConnection(),"Fehler beim Stornieren des Unterauftrags in TAMI !",$row->getChildOrder());
  675.                 }
  676.             }
  677.         }
  678.         // update job
  679.         if ($row->getJob()!==null)
  680.         {
  681.             $jr $this->em->getRepository(Job::class);
  682.             $jr->discardJobs([$row->getJob()->getId()],true);
  683.         }
  684.         // update avail
  685.         $this->updateAvailabilityFromOrder($row);
  686.         // notify
  687.         $this->notifier->notifyAboutOrder($row,false);
  688.         $this->notifier->notifyTelegramAboutOrder($row,false);
  689.         // update remote status
  690.         if ($row->getJob()!==null)
  691.         {
  692.             $member $row->getJob()->getMember();
  693.             if ($member instanceof CoopMember) {
  694.                 if ($member->getXchgTargetUrl() !== "") {
  695.                     $this->cancelInPlatform($row);
  696.                 }
  697.             }
  698.         }
  699.     }
  700.     public function triggerJobCalculation(Job $row)
  701.     {
  702.         /** @var JobRepository $repo */
  703.         $repo $this->em->getRepository(Job::class);
  704.         $this->paymentCalculator->calculateTotals($row$row->isBilled(Billing::TYPE_CUSTOMER),$row->isBilled(Billing::TYPE_MEMBER));
  705.         $msg = [];
  706.         JobRepository::detectReadyForBilling($row$msg);
  707.         return $msg;
  708.     }
  709.     public function getJobPdf(Order $rowUser $requestor$source='',$enforceMember=false)
  710.     {
  711.         if ($enforceMember)
  712.         {
  713.             if  ($requestor->getMember()===null) throw new \Exception('Ihrem Nutzer ist kein Mitglied zugeordnet');
  714.             if ( ($row->getAssignedTo()=== null) ||($row->getAssignedTo()->getId() !== $requestor->getMember()->getId())) throw new \Exception('Fahrt ist Ihnen nicht zugeordnet');
  715.             if (!$row->isAssignmentConfirmed()) throw new \Exception('Bitte bestätigen Sie die Fahrtannahme');
  716.         }
  717.         $fn $row->getOrderId(). '-'$row->getId(). '-'date('Y-m-d_H_i_s').'.pdf';
  718.         $pdfFile $this->tempDir$fn ;
  719.         $pdf = new JobPdf($this->config);
  720.         $pdf->render($row);
  721.         $pdf->Output($pdfFile);
  722.         SysLogRepository::logMessage($this->em->getConnection(),SysLogEntry::SYS_INFO,'Job-PDF downloaded ('.$source.')',[
  723.             'orderId'=>$row->getOrderId(),
  724.             'memberId'=>$requestor->getMember()!== null $requestor->getMember()->getId() : null,
  725.             'source'=>$source
  726.         ],$requestor->getId(),$row->getId());
  727.         if ($row->getJobPdfRequestedOn()===null)
  728.         {
  729.             // prevent a flush, only modify the single field
  730.             $this->em->getConnection()->prepare('UPDATE orders SET job_pdf_requested_on = :req WHERE id = :id AND job_pdf_requested_on IS NULL')->execute([
  731.                 'req' => (new \DateTime())->format('Y-m-d H:i:s'),
  732.                 'id' => $row->getId(),
  733.             ]);
  734.         }
  735.         $this->notifier->notifyAboutOrder($row,false);
  736.         return $pdfFile;
  737.     }
  738.     public function sendToPlatform(?Order $row, ?CoopMember $member)
  739.     {
  740.         $repo $this->em->getRepository(Order::class);
  741.         if ($row === null)
  742.         {
  743.             throw new \RuntimeException("Unbekannte Bestellung");
  744.         }
  745.         if ($member === null)
  746.         {
  747.             throw new \RuntimeException("Unbekanntes Mitglied");
  748.         }
  749.         if ($row->getOrderStatus()->getId() !== OrderStatus::STATUS_OPEN)
  750.         {
  751.             throw new \RuntimeException('Bestellung ist nicht im Status Offen.');
  752.         }
  753.         if ($row->getXchgStatus() !== Order::XCHG_NONE)
  754.         {
  755.             throw new \RuntimeException('Bestellung ist bereits Transferquelle oder -Ziel. Weitervermittlung nicht möglich');
  756.         }
  757.         if ($row->getAssignedTo()!==null)
  758.         {
  759.             throw new \RuntimeException('Fahrt ist aktuell einem Mitglied zugeordnet. Bitte zuerst die Zuordnung entfernen.');
  760.         }
  761.         $client OrderHandler::getClientForMemberWhoLoginsAsCustomer($member);
  762.         // PLACE ORDER
  763.         $item $repo->getSingleAsArray($row->getId());
  764.         Api2Controller::prepare4json($item,false);
  765.         unset($item['id']);
  766.         unset($item['paymentType']);
  767.         $dt = new \DateTime($item['orderTime']);
  768.         $item['orderTime_date'] = $dt->format('Y-m-d');
  769.         $item['orderTime_time'] = $dt->format('H:i');
  770.         $data = [
  771.             "login" => $member->getXchgLogin(),
  772.             "password" => $member->getXchgPassword(),
  773.             "commit"=>true,
  774.             "xchg"=>true,
  775.             "original_order_id" => $item['orderId']
  776.         ];
  777.         $data array_merge($data,$item);
  778.         $res $client->request("POST",rtrim($member->getXchgTargetUrl(),'/')."/orders/place",[
  779.             'headers'=> [
  780.                 "Content-Type"=>"application/json"
  781.             ],
  782.             'body' => json_encode($dataJSON_THROW_ON_ERROR)
  783.         ]);
  784.         $res json_decode($res->getBody(), true512JSON_THROW_ON_ERROR);
  785.         if ($res['success']!==true)
  786.         {
  787.             throw new \RuntimeException('Fehler bei der Übertragung.'.var_export($res,true));
  788.         }
  789.         $row->setXchgTo($member);
  790.         $row->setXchgStatus(Order::XCHG_SENT_TO_OTHER);
  791.         $row->setXchgOrderId($res['data']);
  792.         $repo->flush($row);
  793.     }
  794.     public function acceptOrDeclineToPlatformAndSaveOrderState(Order $row$action)
  795.     {
  796.         if ($row->getXchgStatus() !== Order::XCHG_RECEIVED_FROM_OTHER)
  797.         {
  798.             throw new \RuntimeException('Fahrt nicht aus Fremdsystem');
  799.         }
  800.         if ($row->isXchgConfirmed())
  801.         {
  802.             throw new \RuntimeException('Fahrt ist bereits bestätigt.');
  803.         }
  804.         if (!in_array($action,['accept','decline']))
  805.         {
  806.             throw new \RuntimeException('Unbekannte Aktion');
  807.         }
  808.         if ($action==="decline")
  809.         {
  810.             $canCancel true;
  811.             $canMsg '';
  812.             try {
  813.                 $this->ensureThatOrderCanBeCancelled($row);
  814.             }
  815.             catch (\Throwable $ex)
  816.             {
  817.                 $canCancel false;
  818.                 $canMsg $ex->getMessage();
  819.             }
  820.             if (!$canCancel)
  821.             {
  822.                 throw new \RuntimeException('Stornierung der Fahrt nicht möglich. Daher Ablehnung nicht möglich. (Meldung:'.$canMsg.')');
  823.             }
  824.         }
  825.         $client self::getClientForCustomerWhoLoginsAsMember($row->getCustomer());
  826.         $res $client->request("POST",rtrim($row->getCustomer()->getXchgPlatformUrl(),'/')."/respond-assignment",[
  827.             'headers'=> [
  828.                 "Content-Type"=>"application/json"
  829.             ],
  830.             'body' => json_encode(
  831.                 [
  832.                     "login" => $row->getCustomer()->getXchgLoginAsMember(),
  833.                     "password" => $row->getCustomer()->getXchgLoginPassword(),
  834.                     "action"=> $action,
  835.                     "commit"=>true,
  836.                     "orderId"=> $row->getXchgOrderId(),
  837.                     "xchg"=>true
  838.                 ]
  839.                 , JSON_THROW_ON_ERROR)
  840.         ]);
  841.         $res json_decode($res->getBody(), true512JSON_THROW_ON_ERROR);
  842.         if ($res['success']!==true)
  843.         {
  844.             throw new \RuntimeException('Fehler bei der Übertragung.'.var_export($res,true));
  845.         }
  846.         if ($action==="accept")
  847.         {
  848.             $row->setXchgConfirmed(true);
  849.             $this->em->flush();
  850.         }
  851.         else
  852.         if ($action ==="decline")
  853.         {
  854.             $this->cancelOrder($row);
  855.         }
  856.     }
  857.     protected function cancelInPlatform(Order $row)
  858.     {
  859.         $client OrderHandler::getClientForMemberWhoLoginsAsCustomer($row->getAssignedTo());
  860.         $res $client->request("POST",rtrim($row->getAssignedTo()->getXchgTargetUrl(),'/')."/orders/cancel",[
  861.             'headers'=> [
  862.                 "Content-Type"=>"application/json"
  863.             ],
  864.             'body' => json_encode(
  865.                 [
  866.                     "login" => $row->getAssignedTo()->getXchgLogin(),
  867.                     "password" => $row->getAssignedTo()->getXchgPassword(),
  868.                     "orderId"=> $row->getXchgOrderId(),
  869.                     "commit"=>true,
  870.                     "xchg"=>true
  871.                 ]
  872.                 , JSON_THROW_ON_ERROR)
  873.         ]);
  874.         $res json_decode($res->getBody(), true512JSON_THROW_ON_ERROR);
  875.         if ($res['success']!==true)
  876.         {
  877.             throw new \RuntimeException('Fehler bei der Übertragung.'.var_export($res,true));
  878.         }
  879.     }
  880. }