src/Controller/Api/v3/ExpenseController.php line 825

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Api\v3;
  3. use App\Controller\Api\Service\ExpenseService;
  4. use App\Controller\Api\Service\InvoiceService;
  5. use App\Controller\Api\Service\MaterialService;
  6. use App\Controller\Api\Service\ReferenceService;
  7. use App\Controller\Api\Service\ProjectOrderService;
  8. use App\Controller\Api\Service\SupplierService;
  9. use App\Entity\Invoices;
  10. use App\Entity\Locations;
  11. use App\Entity\ProductCatalog;
  12. use App\Entity\ProjectOrderExpensePositions;
  13. use App\Entity\ProjectOrderExpenses;
  14. use App\Entity\StockItems;
  15. use App\Entity\Unit;
  16. use App\Service\ExpensePdfService;
  17. use Doctrine\ORM\EntityManagerInterface;
  18. use phpDocumentor\Reflection\Types\This;
  19. use Symfony\Component\Mailer\MailerInterface;
  20. use Symfony\Component\Mime\Address;
  21. use Symfony\Component\Mime\Email;
  22. use Symfony\Component\Serializer\SerializerInterface;
  23. use App\Enum\SeverityInterface;
  24. use App\Service\SerializeService\ExpenseSerialize;
  25. use App\Service\SerializeService\InvoiceSerialize;
  26. use App\Service\SerializeService\ReferenceSerialize;
  27. use App\Service\SerializeService\ProjectOrderSerialize;
  28. use App\Service\SerializeService\SupplierSerialize;
  29. use App\Service\StreamingService;
  30. use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
  31. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  32. use Symfony\Component\HttpFoundation\Request;
  33. use Symfony\Component\HttpFoundation\Response;
  34. use Symfony\Component\HttpFoundation\StreamedResponse;
  35. use Symfony\Component\Routing\Annotation\Route;
  36. use Symfony\Contracts\Translation\TranslatorInterface;
  37. use function Symfony\Component\DependencyInjection\Loader\Configurator\param;
  38. /**
  39.  * @Route("/api/v3/expense")
  40.  */
  41. class ExpenseController extends AbstractController
  42. {
  43.     /**
  44.      * STREAMED POSITION
  45.      * @Route("/stream-expense-dependencies", name="api_v3_expense_stream_expense_dependencies")
  46.      * @throws \Exception
  47.      */
  48.     public function streamExpenseDependencies(
  49.         SupplierService        $supplierService,
  50.         SupplierSerialize      $supplierSerialize,
  51.         ProjectOrderService    $projectOrderService,
  52.         ProjectOrderSerialize  $projectOrderSerialize,
  53.         MaterialService        $materialService,
  54.         ReferenceSerialize     $referenceSerialize,
  55.         TranslatorInterface    $translator,
  56.         EntityManagerInterface $manager
  57.     ): StreamedResponse {
  58.         $streamed = new StreamingService([
  59.             "localText" => [
  60.                 "completed" => $translator->trans("Ready...")
  61.             ]
  62.         ]);
  63.         $streamed->appendStream(
  64.             'suppliers',
  65.             $translator->trans("Loading suppliers..."),
  66.             fn() => $supplierSerialize->setEntity($supplierService->fetchAll())->serializeWithFrameworkItems()
  67.         );
  68.         $streamed->appendStream(
  69.             'materials',
  70.             $translator->trans("Loading suppliers..."),
  71.             fn() => $referenceSerialize->setEntity($materialService->fetchAll())->serializeCustomCore("material")
  72.         );
  73.         $streamed->appendStream(
  74.             'unit_types',
  75.             $translator->trans("Loading Units..."),
  76.             fn() => $referenceSerialize->setClassName(Unit::class)->serializeCustomCore("unit_type")
  77.         );
  78.         $streamed->appendStream(
  79.             'product_catalog',
  80.             $translator->trans("Loading product catalog..."),
  81.             //fn() => array_map(
  82.             //    fn(ProductCatalog $c) => [
  83.             //        'internal_code' => $c->getInternalCode(),
  84.             //        'internal_name' => $c->getInternalName(),
  85.             //        'description'   => $c->getDescription(),
  86.             //    ],
  87.             //    $manager->getRepository(ProductCatalog::class)->findBy(['is_active' => true])
  88.             //)
  89.             fn() => $referenceSerialize->setEntity($manager->getRepository(ProductCatalog::class)->findAll())->setGroups([
  90.                 "product_catalog@base"
  91.             ])->render()
  92.         );
  93.         $streamed->appendStream(
  94.             'locations',
  95.             $translator->trans("Loading locations..."),
  96.             fn() => $referenceSerialize->setEntity($manager->getRepository(Locations::class)->findAll())->setGroups([
  97.                 "location@base"
  98.             ])->render()
  99.         );
  100.         //$streamed->appendStream(
  101.         //    'project_orders',
  102.         //    $translator->trans("Loading orders..."),
  103.         //    fn() => $projectOrderSerialize->setEntity($projectOrderService->fetchAll())->serializeForSelector()
  104.         //);
  105.         return $streamed->flush();
  106.     }
  107.     /**
  108.      * STREAMED POSITION
  109.      * @Route("/stream-expense-cart-dataset", name="api_v3_expense_stream_expense_chart_dataset")
  110.      * @throws \Exception
  111.      */
  112.     public function streamExpenseCartDataset(ExpenseService $expenseServiceTranslatorInterface $translator):
  113.     StreamedResponse {
  114.         $streamed = new StreamingService([
  115.             "localText" => [
  116.                 "completed" => $translator->trans("Ready...")
  117.             ]
  118.         ]);
  119.         $streamed->appendStream(
  120.             'metrics',
  121.             $translator->trans("Loading metrics..."),
  122.             fn() => $expenseService->fetchChartDatasetMetrics()
  123.         );
  124.         $streamed->appendStream(
  125.             'by_project',
  126.             $translator->trans("Loading project breakdown..."),
  127.             fn() => $expenseService->fetchChartDatasetByProject()
  128.         );
  129.         $streamed->appendStream(
  130.             'by_supplier',
  131.             $translator->trans("Loading supplier breakdown..."),
  132.             fn() => $expenseService->fetchChartDatasetBySupplier()
  133.         );
  134.         $streamed->appendStream(
  135.             'orders',
  136.             $translator->trans("Loading orders..."),
  137.             fn() => $expenseService->fetchChartDatasetOrders()
  138.         );
  139.         $streamed->appendStream(
  140.             'by_day',
  141.             $translator->trans("Loading daily breakdown..."),
  142.             fn() => $expenseService->fetchChartDatasetByDay()
  143.         );
  144.         $streamed->appendStream(
  145.             'by_week',
  146.             $translator->trans("Loading weekly breakdown..."),
  147.             fn() => $expenseService->fetchChartDatasetByWeek()
  148.         );
  149.         return $streamed->flush();
  150.     }
  151.     /**
  152.      * @Route("/expenses/{page}/{limit}/{search}", name="api_v3_expense_expenses_paginated", methods={"POST"}, defaults={"page": 1, "limit": 5000, "search": null})
  153.      * @throws \Exception
  154.      */
  155.     public function expensesPaginated(ExpenseService $expenseServiceExpenseSerialize $expenseSerializeRequest $requestTranslatorInterface $translatorint $pageint $limit, ?string $search):
  156.     Response
  157.     {
  158.         #dd($page, $limit, $search);
  159.         $pagination $expenseService->fetchAllPaginated($page$limit$search);
  160.         $serializedExpenses $expenseSerialize->setEntity($pagination->getItems())->serializeMainList();
  161.         #dd($serializedExpenses);
  162.         if ($expenseService->getException()) {
  163.             return $this->json([
  164.                 "message" => $expenseService->getException()->getMessage(),
  165.                 "severity" => SeverityInterface::WARNING
  166.             ], Response::HTTP_BAD_REQUEST);
  167.         }
  168.         return $this->json([
  169.             "message"    => $translator->trans('Loaded ranged expenses     successfully'),
  170.             "severity"   => SeverityInterface::SUCCESS,
  171.             "expenses"   => $serializedExpenses,
  172.             "total"      => $pagination->getTotalItemCount(),
  173.             "page"       => $pagination->getCurrentPageNumber(),
  174.             "limit"      => $pagination->getItemNumberPerPage(),
  175.             "totalPages" => (int) ceil($pagination->getTotalItemCount() / $pagination->getItemNumberPerPage()),
  176.         ], Response::HTTP_OK);
  177.     }
  178.     /**
  179.      * Self-repair: recompute expense.total for every persisted expense from
  180.      * its current position data. User triggers this from the list menu when
  181.      * a total looks stale. Returns { message, severity, updated_count }.
  182.      *
  183.      * @Route("/sync-metrics", name="api_v3_expense_sync_metrics", methods={"POST"})
  184.      */
  185.     public function syncMetrics(ExpenseService $expenseServiceTranslatorInterface $translator): Response
  186.     {
  187.         $updated $expenseService->syncAllExpenseTotals();
  188.         return $this->json([
  189.             'message'       => sprintf('%d %s'$updated$translator->trans('expense total(s) recomputed')),
  190.             'severity'      => SeverityInterface::SUCCESS,
  191.             'updated_count' => $updated,
  192.         ], Response::HTTP_OK);
  193.     }
  194.     /**
  195.      * Streamed variant of sync-metrics — SSE events per expense so the FE
  196.      * dialog can render a live progress bar with abort. Mirrors
  197.      * project sync-all-metrics pattern.
  198.      *
  199.      * @Route("/sync-metrics-streamed", name="api_v3_expense_sync_metrics_streamed", methods={"POST"})
  200.      */
  201.     public function syncMetricsStreamed(ExpenseService $expenseServiceRequest $request): StreamedResponse
  202.     {
  203.         $syncToken = (string) $request->request->get('syncToken'uniqid('exp_sync_'true));
  204.         return new StreamedResponse(function () use ($expenseService$syncToken) {
  205.             $emit = function (array $event) {
  206.                 echo 'data: ' json_encode($event) . "\n\n";
  207.                 if (ob_get_level() > 0) {
  208.                     ob_flush();
  209.                 }
  210.                 flush();
  211.             };
  212.             $expenseService->syncAllExpenseTotalsStreamed($syncToken$emit);
  213.         }, Response::HTTP_OK, [
  214.             'Content-Type'      => 'text/event-stream',
  215.             'Cache-Control'     => 'no-cache, no-store',
  216.             'X-Accel-Buffering' => 'no',
  217.             'Connection'        => 'keep-alive',
  218.         ]);
  219.     }
  220.     /**
  221.      * Sibling to sync-metrics-streamed — writes an abort flag file that the
  222.      * running streamed sync polls each iteration. The dialog's Abort button
  223.      * hits this endpoint; the streamed loop then rolls back and emits an
  224.      * 'aborted' event so the client shell can close cleanly.
  225.      *
  226.      * @Route("/abort-sync", name="api_v3_expense_abort_sync", methods={"POST"})
  227.      */
  228.     public function abortSync(ExpenseService $expenseServiceRequest $request): Response
  229.     {
  230.         $syncToken = (string) $request->request->get('syncToken''');
  231.         if ($syncToken === '') {
  232.             return $this->json(['message' => 'syncToken is required''severity' => SeverityInterface::WARNING], Response::HTTP_BAD_REQUEST);
  233.         }
  234.         $expenseService->setAbortFlag($syncToken);
  235.         return $this->json(['message' => 'Abort flag set''severity' => SeverityInterface::SUCCESS], Response::HTTP_OK);
  236.     }
  237.     /**
  238.      * @deprecated Route disabled — replaced by expensesPaginated (KNP)
  239.      * // Route("/expenses/{page}/{limit}/{search}", name="api_v3_expense_expenses", methods={"POST"}, defaults={"page": 1, "limit": 5000, "search": null})
  240.      * @throws \Exception
  241.      */
  242.     public function expensesExpenseService $expenseServiceExpenseSerialize $expenseSerializeRequest $requestTranslatorInterface $translatorint $pageint $limit, ?string $search ):
  243.     Response
  244.     {
  245.         $searchableColumns $request->request->get('filterableColumns');
  246.         $searchableColumns json_decode($searchableColumns);
  247.         $data $expenseService->fetchAll($page$limit$search$searchableColumns);
  248.         $serializedExpenses $expenseSerialize->setEntity($data["data"])->serializeMainList();
  249.         if($expenseService->getException()){
  250.             return $this->json([
  251.                 "message" => $expenseService->getException()->getMessage(),
  252.                 "severity" => SeverityInterface::WARNING
  253.             ], Response::HTTP_BAD_REQUEST );
  254.         }
  255.         return $this->json([
  256.             "message" => $translator->trans('Loaded ranged expenses     successfully'),
  257.             "severity" => SeverityInterface::SUCCESS,
  258.             "expenses" => $serializedExpenses,
  259.             "total" => $data["total"]
  260.         ], Response::HTTP_OK, [], [
  261.             // "groups" => $expenseSerialize->_groupFull()
  262.         ]);
  263.     }
  264.     /**
  265.      * @Route("/fetch/{expense_id}", name="api_v3_expense_fetch", defaults={"expense_id", null})
  266.      * @ParamConverter("expense", options={"mapping": {"expense_id":"id"}})
  267.      * @throws \Exception
  268.      * STREAMED
  269.      */
  270.     public function fetch(?ProjectOrderExpenses $expenseExpenseSerialize $expenseSerializeReferenceService $referenceServiceReferenceSerialize $referenceSerializeSupplierService $supplierServiceSupplierSerialize $supplierSerialize,
  271.                           TranslatorInterface $translatorint $expense_id null
  272.     ):
  273.     ?StreamedResponse {
  274.         #dd($expense);
  275.         $streamed = new StreamingService([
  276.             "localText" => [
  277.                 "completed" => $translator->trans("Ready...")
  278.             ]
  279.         ]);
  280.         $streamed->appendStream(
  281.             'expense',
  282.             $translator->trans("Loading expense..."),
  283.             fn() => $expense $expenseSerialize->setEntity($expense)->serializeFull() : []
  284.         );
  285.         $streamed->appendStream(
  286.             'paperPattern',
  287.             $translator->trans("Loading document pattern..."),
  288.             fn() => $referenceSerialize
  289.                 ->setEntity($referenceService->paperPattern"expense"))
  290.                 ->serializePaperPattern()
  291.         );
  292.         $streamed->appendStream(
  293.             'suppliers',
  294.             $translator->trans("Loading suppliers..."),
  295.             fn() => $supplierSerialize
  296.                 ->setEntity($supplierService->fetchAll())
  297.                 ->serializeFull()
  298.         );
  299.         return $streamed->flush();
  300.     }
  301.     /**
  302.      * @Route("/upsert/{expense_id}", name="api_v3_expense_upsert", methods={"POST"}, defaults={"expense_id": null})
  303.      * @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
  304.      * @throws \Exception
  305.      */
  306.     public function upsert(?ProjectOrderExpenses $expenseExpenseService $expenseServiceExpenseSerialize $expenseSerializeTranslatorInterface $translatorRequest $request, ?int $expense_id ):
  307.     Response
  308.     {
  309.         $params $request->request;
  310.         $files $request->files;
  311.         $expense $expenseService
  312.             ->setParams($params)
  313.             ->setFiles($files)
  314.             ->setEntity($expense$expense_id)->upsert();
  315.         if ($expenseService->getException()) {
  316.             return $this->json([
  317.                 "message" => $expenseService->getException()->getMessage(),
  318.                 "severity" => SeverityInterface::WARNING
  319.             ], Response::HTTP_BAD_REQUEST);
  320.         }
  321.         $serializedExpense  $expenseSerialize->setEntity($expense)->serializeFull();
  322.         return $this->json([
  323.             "message" => $translator->trans('Updated invoices successfully'),
  324.             "severity" => "success",
  325.             "expense" => $serializedExpense
  326.         ]);
  327.     }
  328.     /**
  329.      * ExpenseV2 — expense header + positions toplu upsert.
  330.      * Eski /upsert route'una dokunulmaz; bu route tam uyum sağlandığında eski silinir.
  331.      *
  332.      * @Route("/upsert-v2/{expense_id}", name="api_v3_expense_v2_upsert", methods={"POST"}, defaults={"expense_id": null})
  333.      * @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
  334.      * @throws \Exception
  335.      */
  336.     public function upsertV2(
  337.         ?ProjectOrderExpenses $expense,
  338.         ExpenseService        $expenseService,
  339.         ExpenseSerialize      $expenseSerialize,
  340.         TranslatorInterface   $translator,
  341.         Request               $request,
  342.         ?int                  $expense_id
  343.     ): Response {
  344.         $result $expenseService
  345.             ->setParams($request->request)
  346.             ->setFiles($request->files)
  347.             ->setEntity($expense$expense_id)
  348.             ->upsertV2();
  349.         if ($expenseService->getException()) {
  350.             return $this->json([
  351.                 'message'  => $expenseService->getException()->getMessage(),
  352.                 'severity' => SeverityInterface::WARNING,
  353.             ], Response::HTTP_BAD_REQUEST);
  354.         }
  355.         return $this->json([
  356.             'message'  => $translator->trans('Expense saved successfully'),
  357.             'severity' => SeverityInterface::SUCCESS,
  358.             'expense'  => $expenseSerialize->setEntity($result)->serializeFull(),
  359.         ]);
  360.     }
  361.     /**
  362.      * @deprecated use syncPositions
  363.      * @Route("/remove/{expense_id}", name="api_v3_expense_remove", methods={"POST"}, defaults={"expense_id": null})
  364.      * @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
  365.      * @throws \Exception
  366.      */
  367.     public function remove( ?ProjectOrderExpenses $expenseExpenseService $expenseServiceExpenseSerialize $expenseSerializeTranslatorInterface $translator, ?int $expense_id):
  368.     Response
  369.     {
  370.         #dd($expense_id);
  371.         $removedId $expenseService->removeExpenseAndSync($expense);
  372.         if($expenseService->getException()){
  373.             return $this->json([
  374.                 "message" => $expenseService->getException()->getMessage(),
  375.                 "severity" => SeverityInterface::WARNING
  376.             ], Response::HTTP_BAD_REQUEST );
  377.         }
  378.         return $this->json([
  379.             "message" => $translator->trans('Removed expense successfully'),
  380.             "severity" => SeverityInterface::SUCCESS,
  381.             "id" => $removedId
  382.         ], Response::HTTP_OK);
  383.     }
  384.     /**
  385.      * TODO Depend On Expense Upsert (New Generation Model)
  386.      * TODO New Version Module
  387.      * @Route("/sync-positions/{expense_id}", name="api_v3_expense_sync_positions", methods={"POST"})
  388.      * @ParamConverter("invoice", options={"mapping": {"expense_id": "id"}})
  389.      * @throws \Exception
  390.      */
  391.     public function syncPositions(ExpenseService $expenseServiceExpenseSerialize $expenseSerializeTranslatorInterface $translator, ?int $invoice_id): Response
  392.     {
  393.         // TODO Wait for Development (Sync Depend On Expense)
  394.         return $this->json([
  395.             "message" => $translator->trans('Synced payments successfully'),
  396.             "severity" => "success",
  397.             // "invoice" => $serializedInvoice
  398.             // "amounts" => $amounts
  399.         ]);
  400.     }
  401.     /**
  402.      * @throws \Exception
  403.      *@deprecated us SRP
  404.      * ❌TODO Temporarily Method (SRP) (Old generation model) (SRP Principal)
  405.      * TODO Positionslar Simdilik V0 Fetch ile hallediliyor yani
  406.      * Positionlar yerine Expense Fetch Ediliyor positionlar icinde
  407.      * @Route("/positions/{expense_id}", name="api_v3_expense_positions", methods={"POST"})
  408.      * @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
  409.      */
  410.     public function positionsProjectOrderExpenses $expenseExpenseService $expenseServiceExpenseSerialize $expenseSerializeTranslatorInterface $translator, ?int $expense_id):
  411.     Response
  412.     {
  413.         $serializedExpensePositions $expenseSerialize
  414.             ->setEntity($expense->getProjectOrderExpensePositions())
  415.             // ->setEntity($expense)
  416.             ->serializePositionsFull();
  417.         // TODO Remove this byNew Version
  418.         $serializedExpense $expenseSerialize
  419.             ->setEntity($expense)
  420.             ->serializePositionsFull();
  421.         if($expenseService->getException()){
  422.             return $this->json([
  423.                 "message" => $expenseService->getException()->getMessage(),
  424.                 "severity" => SeverityInterface::WARNING
  425.             ], Response::HTTP_BAD_REQUEST );
  426.         }
  427.         return $this->json([
  428.             "message" => $translator->trans('Loaded expense positions successfully'),
  429.             "severity" => SeverityInterface::SUCCESS,
  430.             // "expensePositions" => $serializedExpensePositions,
  431.             // TODO No use by New Version
  432.             "expenseExpanded" => $serializedExpense,
  433.             "expense" => $serializedExpense,
  434.             "positions" => $serializedExpensePositions,
  435.         ], Response::HTTP_OK);
  436.     }
  437.     /**
  438.      * @deprecated us SRP
  439.      * ❌TODO Maybe Temporarily Method (SRP) (Old generation model) (SRP Principal)
  440.      * @Route("/position-fetch/{expense_id}/{position_id}", name="api_v3_expense_position_fetch", methods={"POST"}, defaults={"position_id": null})
  441.      * @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
  442.      * @ParamConverter("expensePosition", options={"mapping": {"position_id": "id"}})
  443.      * @throws \Exception
  444.      */
  445.     public function positionFetchProjectOrderExpenses $expense, ?ProjectOrderExpensePositions $expensePositionExpenseService $expenseServiceExpenseSerialize $expenseSerializeRequest $requestTranslatorInterface $translator, ?int $position_id):
  446.     Response
  447.     {
  448.         $expenseService
  449.             ->setEntity($expensePosition$position_id)
  450.             ->setParams($request->request)
  451.             ->setFiles($request->files)
  452.             ->upsertPosition($expense);
  453.         if($expenseService->getException()){
  454.             return $this->json([
  455.                 "message" => $expenseService->getException()->getMessage(),
  456.                 "severity" => SeverityInterface::WARNING
  457.             ], Response::HTTP_BAD_REQUEST );
  458.         }
  459.         $expenseExpanded $expenseSerialize->setEntity($expensePosition->getProjectOrderExpense())->serializeFull();
  460.         return $this->json([
  461.             "message" => $translator->trans('Loaded expense positions successfully'),
  462.             "severity" => SeverityInterface::SUCCESS,
  463.             "expenseExpanded" => $expenseExpanded,
  464.         ], Response::HTTP_OK);
  465.     }
  466.     /**
  467.      * STREAMED POSITION
  468.      * @Route("/position-streamed-fetch/{expense_id}/{position_id}", name="api_v3_expense_position_streamed_fetch", requirements={"expense_id":"\d+"}, defaults={"position_id":null})
  469.      * @ParamConverter("position", options={"mapping": {"position_id":"id"}})
  470.      * @ParamConverter("expense", options={"mapping": {"expense_id":"id"}})
  471.      * @throws \Exception
  472.      */
  473.     public function positionStreamedFetch(
  474.         ?ProjectOrderExpensePositions $position,
  475.         ProjectOrderExpenses $expense,
  476.         ExpenseSerialize $expenseSerialize,
  477.         MaterialService $materialService,
  478.         ReferenceSerialize $referenceSerialize,
  479.         SupplierService $supplierService,
  480.         SupplierSerialize $supplierSerialize,
  481.         EntityManagerInterface $manager,
  482.         SerializerInterface $serializer,
  483.         TranslatorInterface $translator
  484.     ): StreamedResponse {
  485.         $streamed = new StreamingService([
  486.             "localText" => [
  487.                 "completed" => $translator->trans("Ready...")
  488.             ]
  489.         ]);
  490.         $streamed->appendStream(
  491.             'position',
  492.             $translator->trans("Loading expense position..."),
  493.             fn() => $position $expenseSerialize->setEntity($position)->serializeFull() : []
  494.         );
  495.         $streamed->appendStream(
  496.             'expense',
  497.             $translator->trans("Loading expense..."),
  498.             fn() => $expense $expenseSerialize->setEntity($expense)->serializeFull() : []
  499.         );
  500.         // Materials with supplier relation for supplier-material mode
  501.         $streamed->appendStream(
  502.             'materials',
  503.             $translator->trans("Loading materials..."),
  504.             fn() => $referenceSerialize->setEntity($materialService->fetchAll())->setGroups([
  505.                 "material@core",
  506.                 "material@supplier",
  507.                 "supplier@base",
  508.             ])->render()
  509.         );
  510.         // Suppliers for supplier-material mode
  511.         $streamed->appendStream(
  512.             'suppliers',
  513.             $translator->trans("Loading suppliers..."),
  514.             fn() => $supplierSerialize->setEntity($supplierService->fetchAll())->serializeBase()
  515.         );
  516.         // Stock items for stock selection mode
  517.         $streamed->appendStream(
  518.             'stock_items',
  519.             $translator->trans("Loading stock items..."),
  520.             fn() => $serializer->normalize(
  521.                 $manager->getRepository(StockItems::class)->findAll(),
  522.                 null,
  523.                 ['groups' => ['stock_item@core']]
  524.             )
  525.         );
  526.         return $streamed->flush();
  527.     }
  528.     /**
  529.      * ❌TODO Temporarily Method (SRP) (Old generation model) (SRP Principal)
  530.      * @Route("/position-upsert/{expense_id}/{position_id}", name="api_v3_expense_position_upsert", methods={"POST"}, defaults={"position_id": null})
  531.      * @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
  532.      * @ParamConverter("expensePosition", options={"mapping": {"position_id": "id"}})
  533.      * @throws \Exception
  534.      */
  535.     public function positionUpsertProjectOrderExpenses $expense, ?ProjectOrderExpensePositions $expensePositionExpenseService $expenseServiceExpenseSerialize $expenseSerializeRequest $requestTranslatorInterface $translator, ?int $position_id):
  536.     Response
  537.     {
  538.         $expensePosition $expenseService
  539.             ->setEntity($expensePosition$position_id)
  540.             ->setParams($request->request)
  541.             ->setFiles($request->files)
  542.             ->upsertPosition($expense);
  543.         if($expenseService->getException()){
  544.             return $this->json([
  545.                 "message" => $expenseService->getException()->getMessage(),
  546.                 "severity" => SeverityInterface::WARNING
  547.             ], Response::HTTP_BAD_REQUEST );
  548.         }
  549.         # dd($expensePosition);
  550.         $expenseExpanded $expenseSerialize->setEntity($expensePosition->getProjectOrderExpense())->serializeFull();
  551.         return $this->json([
  552.             "message" => $translator->trans('Loaded expense positions successfully'),
  553.             "severity" => SeverityInterface::SUCCESS,
  554.             "expenseExpanded" => $expenseExpanded,
  555.         ], Response::HTTP_OK);
  556.     }
  557.     /**
  558.      * @Route("/position-remove/{position_id}", name="api_v3_expense_position_remove", methods={"POST"}, defaults={"position_id": null})
  559.      * @ParamConverter("position", options={"mapping": {"position_id": "id"}})
  560.      * @throws \Exception
  561.      */
  562.     public function positionRemove( ?ProjectOrderExpensePositions $positionExpenseService $expenseServiceExpenseSerialize $expenseSerializeTranslatorInterface $translator, ?int $position_id):
  563.     Response
  564.     {
  565.         $parentExpense $position->getProjectOrderExpense();
  566.         $removedId $expenseService->deletePosition($position);
  567.         if($expenseService->getException()){
  568.             return $this->json([
  569.                 "message" => $expenseService->getException()->getMessage(),
  570.                 "severity" => SeverityInterface::WARNING
  571.             ], Response::HTTP_BAD_REQUEST );
  572.         }
  573.         $expenseExpanded $expenseSerialize->setEntity($parentExpense)->serializeFull();
  574.         return $this->json([
  575.             "message" => $translator->trans('Expense position removed successfully'),
  576.             "severity" => SeverityInterface::SUCCESS,
  577.             "id" => $removedId,
  578.             "expense" => $expenseExpanded
  579.         ], Response::HTTP_OK);
  580.     }
  581.     /**
  582.      * @Route("/order-confirmation-pdf/{expense_id}", name="api_v3_expense_order_confirmation_pdf", methods={"POST"})
  583.      * @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
  584.      */
  585.     public function orderConfirmationPdf(
  586.         ProjectOrderExpenses $expense,
  587.         Request              $request,
  588.         ExpensePdfService    $expensePdfService,
  589.         TranslatorInterface  $translator
  590.     ): Response {
  591.         $excludedIds json_decode($request->request->get('excluded_position_ids''[]'), true);
  592.         if (!is_array($excludedIds)) {
  593.             $excludedIds = [];
  594.         }
  595.         return $this->json([
  596.             'message'  => $translator->trans('PDF generated successfully'),
  597.             'severity' => SeverityInterface::SUCCESS,
  598.             'base64'   => $expensePdfService->generateOrderConfirmationPdf($expense$excludedIds),
  599.         ]);
  600.     }
  601.     /**
  602.      * @Route("/position-table-pdf/{expense_id}", name="api_v3_expense_position_table_pdf", methods={"POST"})
  603.      * @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
  604.      */
  605.     public function positionDetailsPdfTable(
  606.         ProjectOrderExpenses $expense,
  607.         ExpensePdfService    $expensePdfService,
  608.         TranslatorInterface  $translator
  609.     ): Response {
  610.         return $this->json([
  611.             'message'  => $translator->trans('PDF generated successfully'),
  612.             'severity' => SeverityInterface::SUCCESS,
  613.             'base64'   => $expensePdfService->generatePositionTablePdf($expense),
  614.         ]);
  615.     }
  616.     /**
  617.      * @Route("/position-details-pdf/{expense_id}", name="api_v3_expense_position_details_pdf", methods={"POST"})
  618.      * @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
  619.      */
  620.     public function positionDetailsPdf(
  621.         ProjectOrderExpenses $expense,
  622.         Request              $request,
  623.         ExpensePdfService    $expensePdfService,
  624.         TranslatorInterface  $translator
  625.     ): Response {
  626.         $positionIds json_decode($request->request->get('position_ids''[]'), true);
  627.         if (!is_array($positionIds)) {
  628.             $positionIds = [];
  629.         }
  630.         return $this->json([
  631.             'message'  => $translator->trans('PDF generated successfully'),
  632.             'severity' => SeverityInterface::SUCCESS,
  633.             'base64'   => $expensePdfService->generatePositionDetailsPdf($expense$positionIds),
  634.         ]);
  635.     }
  636.     /**
  637.      * Clears expensed_at (unlocks the expense). Requires high-level access — verified by FE before calling.
  638.      *
  639.      * @Route("/unlock/{expense_id}", name="api_v3_expense_unlock", methods={"POST"})
  640.      * @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
  641.      */
  642.     public function unlock(
  643.         ProjectOrderExpenses   $expense,
  644.         EntityManagerInterface $manager,
  645.         ExpenseSerialize       $expenseSerialize,
  646.         TranslatorInterface    $translator
  647.     ): Response {
  648.         $expense->setExpensedAt(null);
  649.         $manager->flush();
  650.         return $this->json([
  651.             'message'  => $translator->trans('Expense unlocked successfully'),
  652.             'severity' => SeverityInterface::SUCCESS,
  653.             'expense'  => $expenseSerialize->setEntity($expense)->serializeFull(),
  654.         ]);
  655.     }
  656.     /**
  657.      * @Route("/send-to-supplier/{expense_id}", name="api_v3_expense_send_to_supplier", methods={"POST"})
  658.      * @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
  659.      */
  660.     public function sendToSupplier(
  661.         ProjectOrderExpenses $expense,
  662.         Request              $request,
  663.         ExpensePdfService    $expensePdfService,
  664.         MailerInterface      $mailer,
  665.         EntityManagerInterface $manager,
  666.         ExpenseSerialize     $expenseSerialize,
  667.         ExpenseService       $expenseService,
  668.         TranslatorInterface  $translator
  669.     ): Response {
  670.         // Gonderim guard'i service'te: supplier email + zaten gonderilip kilitlenmis mi (expensed_at).
  671.         $expenseService->guardSendableToSupplier($expense);
  672.         if ($expenseService->getException() !== null) {
  673.             $exception $expenseService->getException();
  674.             return $this->json([
  675.                 'message'  => $exception->getMessage(),
  676.                 'severity' => SeverityInterface::WARNING,
  677.             ], $exception->getCode());
  678.         }
  679.         $supplier $expense->getSupplier();
  680.         $pdfType     $request->request->get('pdf_type''order_confirmation');
  681.         $excludedIds json_decode($request->request->get('excluded_position_ids''[]'), true);
  682.         if (!is_array($excludedIds)) {
  683.             $excludedIds = [];
  684.         }
  685.         $message = (string) ($request->request->get('message') !== null $request->request->get('message') : '');
  686.         if ($pdfType === 'order_confirmation') {
  687.             $base64   $expensePdfService->generateOrderConfirmationPdf($expense$excludedIds);
  688.             $subject  $translator->trans('Order Confirmation') . ' — ' $expense->getExpenseNr();
  689.             $filename 'order_confirmation_' $expense->getExpenseNr() . '.pdf';
  690.         } else {
  691.             $base64   $expensePdfService->generatePositionTablePdf($expense);
  692.             $subject  $translator->trans('Position Details') . ' — ' $expense->getExpenseNr();
  693.             $filename 'position_details_' $expense->getExpenseNr() . '.pdf';
  694.         }
  695.         $pdfContent base64_decode($base64);
  696.         $bodyHtml   nl2br(htmlspecialchars($message !== '' $message $translator->trans('Please find the attached document.')));
  697.         $email = (new Email())
  698.             ->from(new Address('einkauf@romon-erp.com''Romon Group'))
  699.             ->to($supplier->getEmail())
  700.             ->subject($subject)
  701.             ->html($bodyHtml '<br/><br/>')
  702.             ->attach($pdfContent$filename'application/pdf');
  703.         $mailer->send($email);
  704.         $expense->setExpensedAt(new \DateTimeImmutable());
  705.         $manager->flush();
  706.         return $this->json([
  707.             'message'  => $translator->trans('Document sent successfully'),
  708.             'severity' => SeverityInterface::SUCCESS,
  709.             'expense'  => $expenseSerialize->setEntity($expense)->serializeFull(),
  710.         ]);
  711.     }
  712.     /**
  713.      * @Route("/generate-document-paths/{expense_id}/{position_id}", name="api_v3_expense_generate_document_paths", methods={"POST"}, defaults={"expense_id": null, "position_id": null})
  714.      * @ParamConverter("expense", options={"mapping": {"expense_id": "id"}})
  715.      * @ParamConverter("position", options={"mapping": {"position_id": "id"}})
  716.      * @throws \Exception
  717.      */
  718.     public function generateDocumentAbsolutePath(?ProjectOrderExpenses $expense, ?ProjectOrderExpensePositions $positionTranslatorInterface $translatorExpenseService $expenseService):
  719.     Response
  720.     {
  721.         if(!$expense && $position){
  722.             throw new \Exception("File path generator two of one (Expense or position required)");
  723.         }
  724.         // TODO All 2 Way should be array getFileName -> documents
  725.         $files $position ?
  726.             $position->getDocuments()
  727.             : $expense->getDocuments();
  728.         #dump($files);
  729.         $paths $expenseService->generateDocumentAbsolutePath($files);
  730.         #dd($paths);
  731.         // TODO Wait for Development (Sync Depend On Expense)
  732.         return $this->json([
  733.             "message" => $translator->trans('Files generated successfully'),
  734.             "severity" => "success",
  735.             "paths" => $paths,
  736.             "headerName" => $position $position->getProjectOrder()->getOrderNr() : $expense->getExpenseNr()
  737.         ]);
  738.     }
  739. }