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.         // hard 1 year limit
  525.         $earliest = new \DateTime();
  526.         $earliest->sub(new \DateInterval(sprintf("P%dY"1)));
  527.         if ($order->getOrderTime() < $earliest)
  528.         {
  529.             throw new OrderValidationException('Bestellung liegt zu weit in der Vergangenheit');
  530.         }
  531.         $newOrderCreated false;
  532.         if ($order->getOrderStatus()->getId()===OrderStatus::STATUS_DRAFT)
  533.         {
  534.             if ($order->getOrderId()!=='')
  535.             {
  536.                 throw new OrderValidationException('initiateOrder(): order already has an order id !');
  537.             }
  538.             $open $this->em->find(OrderStatus::class,OrderStatus::STATUS_OPEN);
  539.             self::transactionalWithTableLock($this->em"orders", function(EntityManagerInterface $em) use($order,$open)
  540.             {
  541.                 $newId $this->repo->getNewOrderId$order->getOrderTime() );
  542.                 $order->setOrderId($newId);
  543.                 $order->setOrderStatus($open);
  544.                 $order->setOrderInitiatedOn(new \DateTime());
  545.             });
  546. //            $this->em->transactional(function(EntityManagerInterface $em) use($order,$open) {
  547. //                $em->getConnection()->exec('LOCK TABLES orders WRITE;');
  548. //                $newId = $this->repo->getNewOrderId( $order->getOrderTime() );
  549. //                $order->setOrderId($newId);
  550. //                $order->setOrderStatus($open);
  551. //                $order->setOrderInitiatedOn(new \DateTime());
  552. //                $em->getConnection()->exec('UNLOCK TABLES;');
  553. //            });
  554.             $newOrderCreated true;
  555.         }
  556.         else
  557.         {
  558.             if ($order->getOrderId()==='')
  559.             {
  560.                 throw new OrderValidationException('initiateOrder(): order has no order id !');
  561.             }
  562.         }
  563.         // update existing tami
  564.         if ( ($this->USE_TAMI/*|| ($order->getRemoteStatus() !== Order::REMOTE_PENDING)*/ )
  565.         {
  566.             if (!$this->tami->submitOrder($order))
  567.             {
  568.                 SysLogRepository::logError($this->em->getConnection(),"Übertragung nach TaMi fehlgeschlagen: ".$order->getRemoteResult(),$order);
  569.             }
  570.         }
  571.         // create job
  572.         if ($order->getJob()===null)
  573.         {
  574.             $order->setJobJob::createForOrder($order ) );
  575.             if ($this->config->isJobAlwaysRecalculateCustomerPrice())
  576.             {
  577.                 $order->getJob()->setRecalculateCustomerPrice(true);
  578.             }
  579.         }
  580.         else
  581.         {
  582.             $order->getJob()->updateFromOrder($order);
  583.         }
  584.         $this->em->flush(); // update order
  585.         // email confirmation
  586.         if (!$suppressMail)
  587.         {
  588.             try
  589.             {
  590.                 $this->mailHelper->OrderConfirmationMail($order);
  591.             }
  592.             catch (\Throwable $ex)
  593.             {
  594.                 SysLogRepository::logError($this->em->getConnection(),"Mailzustellung bei Bestellung fehlgeschlagen: ".$ex->getMessage(),$order,0,'Mail');
  595.             }
  596.         }
  597.         // emit notification
  598.         $this->notifier->notifyAboutOrder($order$newOrderCreated);
  599.         $this->notifier->notifyTelegramAboutOrder($order,$newOrderCreated);
  600.         // update availability
  601.         $this->updateAvailabilityFromOrder($order);
  602.         // todo: child order -> move from controller to here if possible
  603.         // problem: when not doing in controller, no redirect to actual order occurs, "/new" order is created everytime on a
  604.         // subsequent error
  605. //        if ($order->getChildOrder()!==null)
  606. //        {
  607. //            $this->initiateOrder($order->getChildOrder());
  608. //        }
  609.     }
  610.     public  function isLagTooLow(\DateTime $dt)
  611.     {
  612.         return false// TODO: re-enable temporarily removed check
  613.         if ($this->security->isGranted(Role::NO_TIME_RESTRICTION))
  614.         {
  615.             return false;
  616.         }
  617.         $earliest = new \DateTime();
  618.         $earliest->add(new \DateInterval(sprintf("PT%dH"Order::TIME_LAG_HOURS)));
  619.         return ($dt $earliest);
  620.     }
  621.     public function ensureThatOrderCanBeCancelled(Order $row): void
  622.     {
  623.         if (null !== $row->getReferencedParentOrder())
  624.         {
  625.             throw new OrderValidationException("Diese Fahrt wurde automatisch angelegt und kann nicht gelöscht/storniert werden. Bitte bearbeiten Sie den Hauptauftrag !");
  626.         }
  627.         if ($this->IsLagTooLow($row->getOrderTime()))
  628.         {
  629.             throw new OrderValidationException('order.edit-not-possible-lag');
  630.         }
  631.         if (
  632.             (!$this->security->isGranted(Role::EDIT_CLOSED_ORDERS))
  633.             &&
  634.             (!in_array($row->getOrderStatus()->getId(), [OrderStatus::STATUS_OPENOrderStatus::STATUS_DRAFTOrderStatus::STATUS_VERMITTELT], true))
  635.         )
  636.         {
  637.             throw new OrderValidationException('Fahrt kann nicht mehr storniert werden.');
  638.         }
  639.         if ( ($row->getAssignedTo()!==null) )
  640.         {
  641.             throw new OrderValidationException('Fahrt ist bereits zugeordnet. Bitte rufen Sie die Disposition an um diesen Auftrag zu stornieren');
  642.         }
  643.         if ($row->isAssignmentConfirmed() && ($row->getAssignedTo()!==null))
  644.         {
  645.             throw new OrderValidationException('Fahrt ist bereits zugeordnet. Bitte rufen Sie die Disposition an um diesen Auftrag zu stornieren');
  646.         }
  647.         if ($row->getChildOrder()!==null)
  648.         {
  649.             if ($this->IsLagTooLow$row->getChildOrder()->getOrderTime() ))
  650.             {
  651.                 throw new OrderValidationException('Unterauftrag: order.edit-not-possible-lag');
  652.             }
  653.             if ($row->getChildOrder()->isAssignmentConfirmed() && ($row->getChildOrder()->getAssignedTo()!==null))
  654.             {
  655.                 throw new OrderValidationException('Unterauftrag ist bereits zugeordnet. Bitte rufen Sie die Disposition an um diesen Auftrag zu stornieren');
  656.             }
  657.         }
  658.     }
  659.     public function cancelOrder(Order $row)
  660.     {
  661.         $this->ensureThatOrderCanBeCancelled($row);
  662.         $storniert $this->em->find(OrderStatus::class,OrderStatus::STATUS_CANCELED);
  663.         $this->em->getConnection()->setAutoCommit(false);
  664.         $this->em->transactional(function(EntityManagerInterface $em) use($row,$storniert) {
  665.             $em->getConnection()->exec('LOCK TABLES orders WRITE;');
  666.             $row->setOrderStatus($storniert);
  667.             if ($row->getChildOrder()!==null)
  668.             {
  669.                 $row->getChildOrder()->setOrderStatus($storniert);
  670.             }
  671.             $em->getConnection()->exec('UNLOCK TABLES;');
  672.         });
  673.         $this->em->getConnection()->setAutoCommit(true);
  674.         // update existing tami
  675.         if ( ($this->USE_TAMI) && ($row->getRemoteStatus() !== Order::REMOTE_PENDING))
  676.         {
  677.             if (!$this->tami->cancelOrder($row))
  678.             {
  679.                 SysLogRepository::logError($this->em->getConnection(),"Fehler beim Stornieren des Auftrags in TAMI !",$row);
  680.             }
  681.             if ( ($row->getChildOrder()!==null) && ($this->USE_TAMI) && ($row->getChildOrder()->getRemoteStatus() !== Order::REMOTE_PENDING) )
  682.             {
  683.                 if (!$this->tami->cancelOrder($row))
  684.                 {
  685.                     SysLogRepository::logError($this->em->getConnection(),"Fehler beim Stornieren des Unterauftrags in TAMI !",$row->getChildOrder());
  686.                 }
  687.             }
  688.         }
  689.         // update job
  690.         if ($row->getJob()!==null)
  691.         {
  692.             $jr $this->em->getRepository(Job::class);
  693.             $jr->discardJobs([$row->getJob()->getId()],true);
  694.         }
  695.         // update avail
  696.         $this->updateAvailabilityFromOrder($row);
  697.         // notify
  698.         $this->notifier->notifyAboutOrder($row,false);
  699.         $this->notifier->notifyTelegramAboutOrder($row,false);
  700.         // update remote status
  701.         if ($row->getJob()!==null)
  702.         {
  703.             $member $row->getJob()->getMember();
  704.             if ($member instanceof CoopMember) {
  705.                 if ($member->getXchgTargetUrl() !== "") {
  706.                     $this->cancelInPlatform($row);
  707.                 }
  708.             }
  709.         }
  710.     }
  711.     public function triggerJobCalculation(Job $row)
  712.     {
  713.         /** @var JobRepository $repo */
  714.         $repo $this->em->getRepository(Job::class);
  715.         $this->paymentCalculator->calculateTotals($row$row->isBilled(Billing::TYPE_CUSTOMER),$row->isBilled(Billing::TYPE_MEMBER));
  716.         $msg = [];
  717.         JobRepository::detectReadyForBilling($row$msg);
  718.         return $msg;
  719.     }
  720.     public function getJobPdf(Order $rowUser $requestor$source='',$enforceMember=false)
  721.     {
  722.         if ($enforceMember)
  723.         {
  724.             if  ($requestor->getMember()===null) throw new \Exception('Ihrem Nutzer ist kein Mitglied zugeordnet');
  725.             if ( ($row->getAssignedTo()=== null) ||($row->getAssignedTo()->getId() !== $requestor->getMember()->getId())) throw new \Exception('Fahrt ist Ihnen nicht zugeordnet');
  726.             if (!$row->isAssignmentConfirmed()) throw new \Exception('Bitte bestätigen Sie die Fahrtannahme');
  727.         }
  728.         $fn $row->getOrderId(). '-'$row->getId(). '-'date('Y-m-d_H_i_s').'.pdf';
  729.         $pdfFile $this->tempDir$fn ;
  730.         $pdf = new JobPdf($this->config);
  731.         $pdf->render($row);
  732.         $pdf->Output($pdfFile);
  733.         SysLogRepository::logMessage($this->em->getConnection(),SysLogEntry::SYS_INFO,'Job-PDF downloaded ('.$source.')',[
  734.             'orderId'=>$row->getOrderId(),
  735.             'memberId'=>$requestor->getMember()!== null $requestor->getMember()->getId() : null,
  736.             'source'=>$source
  737.         ],$requestor->getId(),$row->getId());
  738.         if ($row->getJobPdfRequestedOn()===null)
  739.         {
  740.             // prevent a flush, only modify the single field
  741.             $this->em->getConnection()->prepare('UPDATE orders SET job_pdf_requested_on = :req WHERE id = :id AND job_pdf_requested_on IS NULL')->execute([
  742.                 'req' => (new \DateTime())->format('Y-m-d H:i:s'),
  743.                 'id' => $row->getId(),
  744.             ]);
  745.         }
  746.         $this->notifier->notifyAboutOrder($row,false);
  747.         return $pdfFile;
  748.     }
  749.     public function sendToPlatform(?Order $row, ?CoopMember $member)
  750.     {
  751.         $repo $this->em->getRepository(Order::class);
  752.         if ($row === null)
  753.         {
  754.             throw new \RuntimeException("Unbekannte Bestellung");
  755.         }
  756.         if ($member === null)
  757.         {
  758.             throw new \RuntimeException("Unbekanntes Mitglied");
  759.         }
  760.         if ($row->getOrderStatus()->getId() !== OrderStatus::STATUS_OPEN)
  761.         {
  762.             throw new \RuntimeException('Bestellung ist nicht im Status Offen.');
  763.         }
  764.         if ($row->getXchgStatus() !== Order::XCHG_NONE)
  765.         {
  766.             throw new \RuntimeException('Bestellung ist bereits Transferquelle oder -Ziel. Weitervermittlung nicht möglich');
  767.         }
  768.         if ($row->getAssignedTo()!==null)
  769.         {
  770.             throw new \RuntimeException('Fahrt ist aktuell einem Mitglied zugeordnet. Bitte zuerst die Zuordnung entfernen.');
  771.         }
  772.         $client OrderHandler::getClientForMemberWhoLoginsAsCustomer($member);
  773.         // PLACE ORDER
  774.         $item $repo->getSingleAsArray($row->getId());
  775.         Api2Controller::prepare4json($item,false);
  776.         unset($item['id']);
  777.         unset($item['paymentType']);
  778.         $dt = new \DateTime($item['orderTime']);
  779.         $item['orderTime_date'] = $dt->format('Y-m-d');
  780.         $item['orderTime_time'] = $dt->format('H:i');
  781.         $data = [
  782.             "login" => $member->getXchgLogin(),
  783.             "password" => $member->getXchgPassword(),
  784.             "commit"=>true,
  785.             "xchg"=>true,
  786.             "original_order_id" => $item['orderId']
  787.         ];
  788.         $data array_merge($data,$item);
  789.         $res $client->request("POST",rtrim($member->getXchgTargetUrl(),'/')."/orders/place",[
  790.             'headers'=> [
  791.                 "Content-Type"=>"application/json"
  792.             ],
  793.             'body' => json_encode($dataJSON_THROW_ON_ERROR)
  794.         ]);
  795.         $res json_decode($res->getBody(), true512JSON_THROW_ON_ERROR);
  796.         if ($res['success']!==true)
  797.         {
  798.             throw new \RuntimeException('Fehler bei der Übertragung.'.var_export($res,true));
  799.         }
  800.         $row->setXchgTo($member);
  801.         $row->setXchgStatus(Order::XCHG_SENT_TO_OTHER);
  802.         $row->setXchgOrderId($res['data']);
  803.         $repo->flush($row);
  804.     }
  805.     public function acceptOrDeclineToPlatformAndSaveOrderState(Order $row$action)
  806.     {
  807.         if ($row->getXchgStatus() !== Order::XCHG_RECEIVED_FROM_OTHER)
  808.         {
  809.             throw new \RuntimeException('Fahrt nicht aus Fremdsystem');
  810.         }
  811.         if ($row->isXchgConfirmed())
  812.         {
  813.             throw new \RuntimeException('Fahrt ist bereits bestätigt.');
  814.         }
  815.         if (!in_array($action,['accept','decline']))
  816.         {
  817.             throw new \RuntimeException('Unbekannte Aktion');
  818.         }
  819.         if ($action==="decline")
  820.         {
  821.             $canCancel true;
  822.             $canMsg '';
  823.             try {
  824.                 $this->ensureThatOrderCanBeCancelled($row);
  825.             }
  826.             catch (\Throwable $ex)
  827.             {
  828.                 $canCancel false;
  829.                 $canMsg $ex->getMessage();
  830.             }
  831.             if (!$canCancel)
  832.             {
  833.                 throw new \RuntimeException('Stornierung der Fahrt nicht möglich. Daher Ablehnung nicht möglich. (Meldung:'.$canMsg.')');
  834.             }
  835.         }
  836.         $client self::getClientForCustomerWhoLoginsAsMember($row->getCustomer());
  837.         $res $client->request("POST",rtrim($row->getCustomer()->getXchgPlatformUrl(),'/')."/respond-assignment",[
  838.             'headers'=> [
  839.                 "Content-Type"=>"application/json"
  840.             ],
  841.             'body' => json_encode(
  842.                 [
  843.                     "login" => $row->getCustomer()->getXchgLoginAsMember(),
  844.                     "password" => $row->getCustomer()->getXchgLoginPassword(),
  845.                     "action"=> $action,
  846.                     "commit"=>true,
  847.                     "orderId"=> $row->getXchgOrderId(),
  848.                     "xchg"=>true
  849.                 ]
  850.                 , JSON_THROW_ON_ERROR)
  851.         ]);
  852.         $res json_decode($res->getBody(), true512JSON_THROW_ON_ERROR);
  853.         if ($res['success']!==true)
  854.         {
  855.             throw new \RuntimeException('Fehler bei der Übertragung.'.var_export($res,true));
  856.         }
  857.         if ($action==="accept")
  858.         {
  859.             $row->setXchgConfirmed(true);
  860.             $this->em->flush();
  861.         }
  862.         else
  863.         if ($action ==="decline")
  864.         {
  865.             $this->cancelOrder($row);
  866.         }
  867.     }
  868.     protected function cancelInPlatform(Order $row)
  869.     {
  870.         $client OrderHandler::getClientForMemberWhoLoginsAsCustomer($row->getAssignedTo());
  871.         $res $client->request("POST",rtrim($row->getAssignedTo()->getXchgTargetUrl(),'/')."/orders/cancel",[
  872.             'headers'=> [
  873.                 "Content-Type"=>"application/json"
  874.             ],
  875.             'body' => json_encode(
  876.                 [
  877.                     "login" => $row->getAssignedTo()->getXchgLogin(),
  878.                     "password" => $row->getAssignedTo()->getXchgPassword(),
  879.                     "orderId"=> $row->getXchgOrderId(),
  880.                     "commit"=>true,
  881.                     "xchg"=>true
  882.                 ]
  883.                 , JSON_THROW_ON_ERROR)
  884.         ]);
  885.         $res json_decode($res->getBody(), true512JSON_THROW_ON_ERROR);
  886.         if ($res['success']!==true)
  887.         {
  888.             throw new \RuntimeException('Fehler bei der Übertragung.'.var_export($res,true));
  889.         }
  890.     }
  891. }