src/Controller/Api/v3/MaterialRequestController.php line 339

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Api\v3;
  3. use App\Controller\Api\Service\MaterialRequestService;
  4. use App\Entity\MaterialRequest;
  5. use App\Entity\MaterialRequestDocument;
  6. use App\Entity\ProjectOrderTaskFulfillment;
  7. use App\Enum\SeverityInterface;
  8. use App\Service\SerializeService\ItemGroupsSerialize;
  9. use App\Service\SerializeService\MaterialRequestDocumentSerialize;
  10. use App\Service\SerializeService\MaterialRequestSerialize;
  11. use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
  12. use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
  13. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  14. use Symfony\Component\HttpFoundation\Request;
  15. use Symfony\Component\HttpFoundation\Response;
  16. use Symfony\Component\Routing\Annotation\Route;
  17. use Symfony\Contracts\Translation\TranslatorInterface;
  18. /**
  19.  * MaterialRequest — in-house (execution_scope=internal) task execution'inda responsible
  20.  * person'in actigi malzeme siparis talebi. PDF'ler sistem tarafindan PARSE EDILMEZ;
  21.  * Expenses sorumlusu her PDF'i gozle inceleyip PDF SEVIYESINDE karar verir.
  22.  *
  23.  * Yanit sozlesmesi (FE icin):
  24.  *   - upsert/submit/documentUpload/documentDelete -> "materialRequest" (tam nesne, documents dahil)
  25.  *   - documentUpload ayrica "document" (yeni eklenen PDF, tek basina) doner
  26.  *   - review -> "document" (karar verilen PDF) + "materialRequest" (turetilmis header durumu)
  27.  *   - listByExecution -> "materialRequests" (dizi)
  28.  *   - neededCollections -> duz JSON "item_groups" (tek dataset — StreamedResponse DEGIL)
  29.  *
  30.  * Payload alan adlari raw entity property adiyla birebir (alias yasak):
  31.  *   upsert: task_execution (id) · documentUpload: name, item_group (id), file · review: status, review_note
  32.  *
  33.  * 🪤 ASAGIDAKI IKI SATIRDA "@" YOK, BILEREK. Doctrine'in anotasyon okuyucusu
  34.  * docblock'taki HER "@Ad" dizisini GERCEK ANOTASYON sanar. Burada anlatim amacli
  35.  * yazilmis iki IsGranted vardi; parantezsiz olduklari icin SINIF SEVIYESINDE
  36.  * bos attribute listesiyle iki anotasyon uretiyorlardi ve bu
  37.  * controller'daki HER UC 403 donuyordu — voter'a bile ulasilmadan.
  38.  * (Gercek vaka 2026-08-20: gelen kutusu ucu ilk kez cagrildiginda ortaya cikti;
  39.  * bu controller daha once hic calistirilmamisti, o yuzden fark edilmemisti.)
  40.  * Anotasyonu metin icinde anmak gerekirse basina "@" KONMAZ.
  41.  *
  42.  * Requester tarafi (upsert/submit/document.*): IsGranted SECTION:order:task:u + BE'de
  43.  * gercek atama kontrolu (execution.branch_contact_person === oturumdaki worker).
  44.  * Reviewer tarafi (review): IsGranted MODULE:expense:u (mevcut permission deseni,
  45.  * ModulesAccessMap'te YENI key ACILMADI).
  46.  *
  47.  * @Route("/api/v3/material-request")
  48.  */
  49. class MaterialRequestController extends AbstractController
  50. {
  51.     /**
  52.      * id null -> yeni draft talep (body: task_execution). id dolu -> mevcut taslak.
  53.      *
  54.      * @Route("/upsert/{id}", name="api_v3_material_request_upsert", methods={"POST"}, defaults={"id": null})
  55.      * @ParamConverter("materialRequest", options={"mapping": {"id": "id"}})
  56.      * @IsGranted("SECTION:order:task:u")
  57.      */
  58.     public function upsert(
  59.         ?MaterialRequest $materialRequest,
  60.         Request $request,
  61.         TranslatorInterface $translator,
  62.         MaterialRequestService $service,
  63.         MaterialRequestSerialize $serialize
  64.     ): Response {
  65.         try {
  66.             $materialRequest = $service->setParams($request->request)->upsert($materialRequest);
  67.         } catch (\DomainException $e) {
  68.             return $this->json([
  69.                 'message'  => $translator->trans($e->getMessage()),
  70.                 'severity' => SeverityInterface::ERROR,
  71.             ], Response::HTTP_CONFLICT);
  72.         }
  73.         return $this->json([
  74.             'message'         => $translator->trans('Material request saved successfully'),
  75.             'severity'        => SeverityInterface::SUCCESS,
  76.             'materialRequest' => $serialize->setEntity($materialRequest)->serializeFull(),
  77.         ], Response::HTTP_OK);
  78.     }
  79.     /**
  80.      * @Route("/document/upload/{request_id}", name="api_v3_material_request_document_upload", methods={"POST"})
  81.      * @ParamConverter("materialRequest", options={"mapping": {"request_id": "id"}})
  82.      * @IsGranted("SECTION:order:task:u")
  83.      */
  84.     public function documentUpload(
  85.         ?MaterialRequest $materialRequest,
  86.         Request $request,
  87.         TranslatorInterface $translator,
  88.         MaterialRequestService $service,
  89.         MaterialRequestSerialize $serialize,
  90.         MaterialRequestDocumentSerialize $documentSerialize
  91.     ): Response {
  92.         if (!$materialRequest) {
  93.             return $this->json([
  94.                 'message'  => $translator->trans('Material request not found'),
  95.                 'severity' => SeverityInterface::ERROR,
  96.             ], Response::HTTP_NOT_FOUND);
  97.         }
  98.         try {
  99.             $document = $service
  100.                 ->setParams($request->request)
  101.                 ->setFiles($request->files)
  102.                 ->uploadDocument($materialRequest);
  103.         } catch (\DomainException $e) {
  104.             return $this->json([
  105.                 'message'  => $translator->trans($e->getMessage()),
  106.                 'severity' => SeverityInterface::ERROR,
  107.             ], Response::HTTP_UNPROCESSABLE_ENTITY);
  108.         }
  109.         return $this->json([
  110.             'message'         => $translator->trans('Document uploaded successfully'),
  111.             'severity'        => SeverityInterface::SUCCESS,
  112.             'document'        => $documentSerialize->setEntity($document)->serializeFull(),
  113.             'materialRequest' => $serialize->setEntity($document->getMaterialRequest())->serializeFull(),
  114.         ], Response::HTTP_OK);
  115.     }
  116.     /**
  117.      * @Route("/document/delete/{document_id}", name="api_v3_material_request_document_delete", methods={"POST"})
  118.      * @ParamConverter("document", options={"mapping": {"document_id": "id"}})
  119.      * @IsGranted("SECTION:order:task:u")
  120.      */
  121.     public function documentDelete(
  122.         ?MaterialRequestDocument $document,
  123.         TranslatorInterface $translator,
  124.         MaterialRequestService $service,
  125.         MaterialRequestSerialize $serialize
  126.     ): Response {
  127.         if (!$document) {
  128.             return $this->json([
  129.                 'message'  => $translator->trans('Document not found'),
  130.                 'severity' => SeverityInterface::ERROR,
  131.             ], Response::HTTP_NOT_FOUND);
  132.         }
  133.         try {
  134.             $materialRequest = $service->deleteDocument($document);
  135.         } catch (\DomainException $e) {
  136.             return $this->json([
  137.                 'message'  => $translator->trans($e->getMessage()),
  138.                 'severity' => SeverityInterface::ERROR,
  139.             ], Response::HTTP_CONFLICT);
  140.         }
  141.         return $this->json([
  142.             'message'         => $translator->trans('Document removed successfully'),
  143.             'severity'        => SeverityInterface::SUCCESS,
  144.             'materialRequest' => $serialize->setEntity($materialRequest)->serializeFull(),
  145.         ], Response::HTTP_OK);
  146.     }
  147.     /**
  148.      * draft -> submitted. En az 1 dokuman sarti (Service'te zorlanir).
  149.      *
  150.      * @Route("/submit/{id}", name="api_v3_material_request_submit", methods={"POST"})
  151.      * @ParamConverter("materialRequest", options={"mapping": {"id": "id"}})
  152.      * @IsGranted("SECTION:order:task:u")
  153.      */
  154.     public function submit(
  155.         ?MaterialRequest $materialRequest,
  156.         TranslatorInterface $translator,
  157.         MaterialRequestService $service,
  158.         MaterialRequestSerialize $serialize
  159.     ): Response {
  160.         if (!$materialRequest) {
  161.             return $this->json([
  162.                 'message'  => $translator->trans('Material request not found'),
  163.                 'severity' => SeverityInterface::ERROR,
  164.             ], Response::HTTP_NOT_FOUND);
  165.         }
  166.         try {
  167.             $materialRequest = $service->submit($materialRequest);
  168.         } catch (\DomainException $e) {
  169.             return $this->json([
  170.                 'message'  => $translator->trans($e->getMessage()),
  171.                 'severity' => SeverityInterface::ERROR,
  172.             ], Response::HTTP_UNPROCESSABLE_ENTITY);
  173.         }
  174.         return $this->json([
  175.             'message'         => $translator->trans('Material request submitted successfully'),
  176.             'severity'        => SeverityInterface::SUCCESS,
  177.             'materialRequest' => $serialize->setEntity($materialRequest)->serializeFull(),
  178.         ], Response::HTTP_OK);
  179.     }
  180.     /**
  181.      * Karar bildirimi e-postasini talep sahibine gonderir (body: language).
  182.      *
  183.      * AYRI UC, `review`'a GOMULU DEGIL: karar BELGE basina veriliyor, bildirim ise
  184.      * KAYIT BASINA tek ozet ve kullanicinin ONAYINA bagli (dili de o seciyor).
  185.      * Ikisini birlestirmek her belgede bir mail demek olurdu.
  186.      *
  187.      * @Route("/notify-decision/{request_id}", name="api_v3_material_request_notify_decision", methods={"POST"})
  188.      * @ParamConverter("materialRequest", options={"mapping": {"request_id": "id"}})
  189.      * @IsGranted("MODULE:expense:u")
  190.      */
  191.     public function notifyDecision(
  192.         ?MaterialRequest $materialRequest,
  193.         Request $request,
  194.         TranslatorInterface $translator,
  195.         MaterialRequestService $service,
  196.         MaterialRequestSerialize $serialize
  197.     ): Response {
  198.         if (!$materialRequest) {
  199.             return $this->json([
  200.                 'message'  => $translator->trans('Material request not found'),
  201.                 'severity' => SeverityInterface::ERROR,
  202.             ], Response::HTTP_NOT_FOUND);
  203.         }
  204.         try {
  205.             $materialRequest = $service->setParams($request->request)->notifyDecision($materialRequest);
  206.         } catch (\DomainException $e) {
  207.             return $this->json([
  208.                 'message'  => $translator->trans($e->getMessage()),
  209.                 'severity' => SeverityInterface::ERROR,
  210.             ], Response::HTTP_UNPROCESSABLE_ENTITY);
  211.         }
  212.         /* Gonderilmediyse (alici yok / ayar kapali) `last_decision_mail_at` NULL kalir;
  213.            FE bunu okuyup kullaniciya durumu soyleyebilsin diye kayit yine donuyor. */
  214.         return $this->json([
  215.             'message'         => $materialRequest->getLastDecisionMailAt() !== null
  216.                 ? $translator->trans('Email sent successfully')
  217.                 : $translator->trans('No recipient has email notifications enabled'),
  218.             'severity'        => $materialRequest->getLastDecisionMailAt() !== null
  219.                 ? SeverityInterface::SUCCESS
  220.                 : SeverityInterface::WARNING,
  221.             'materialRequest' => $serialize->setEntity($materialRequest)->serializeFull(),
  222.         ], Response::HTTP_OK);
  223.     }
  224.     /**
  225.      * Expenses sorumlusunun PDF-seviyesi karari (body: status + review_note).
  226.      *
  227.      * @Route("/review/{document_id}", name="api_v3_material_request_review", methods={"POST"})
  228.      * @ParamConverter("document", options={"mapping": {"document_id": "id"}})
  229.      * @IsGranted("MODULE:expense:u")
  230.      */
  231.     public function review(
  232.         ?MaterialRequestDocument $document,
  233.         Request $request,
  234.         TranslatorInterface $translator,
  235.         MaterialRequestService $service,
  236.         MaterialRequestSerialize $serialize,
  237.         MaterialRequestDocumentSerialize $documentSerialize
  238.     ): Response {
  239.         if (!$document) {
  240.             return $this->json([
  241.                 'message'  => $translator->trans('Document not found'),
  242.                 'severity' => SeverityInterface::ERROR,
  243.             ], Response::HTTP_NOT_FOUND);
  244.         }
  245.         try {
  246.             $document = $service->setParams($request->request)->review($document);
  247.         } catch (\DomainException $e) {
  248.             return $this->json([
  249.                 'message'  => $translator->trans($e->getMessage()),
  250.                 'severity' => SeverityInterface::ERROR,
  251.             ], Response::HTTP_UNPROCESSABLE_ENTITY);
  252.         }
  253.         return $this->json([
  254.             'message'         => $translator->trans('Decision saved successfully'),
  255.             'severity'        => SeverityInterface::SUCCESS,
  256.             'document'        => $documentSerialize->setEntity($document)->serializeFull(),
  257.             'materialRequest' => $serialize->setEntity($document->getMaterialRequest())->serializeFull(),
  258.         ], Response::HTTP_OK);
  259.     }
  260.     /**
  261.      * Responsible person'in kendi execution'ina acilan talepleri takip etmesi icin.
  262.      * Atama filtresi BE'de (Service::listByExecution -> assertRequesterOwnership).
  263.      *
  264.      * FE (te-frontend) bu ucu duz `HttpRequest(..., "GET")` ile cagiriyor (tek dataset,
  265.      * CLAUDE "tek dataset -> HttpRequest" kurali) -> `methods` KISITLANMAZ (GET calisir).
  266.      *
  267.      * @Route("/list-by-execution/{execution_id}", name="api_v3_material_request_list_by_execution")
  268.      * @ParamConverter("execution", options={"mapping": {"execution_id": "id"}})
  269.      * @IsGranted("SECTION:order:task:u")
  270.      */
  271.     public function listByExecution(
  272.         ?ProjectOrderTaskFulfillment $execution,
  273.         TranslatorInterface $translator,
  274.         MaterialRequestService $service,
  275.         MaterialRequestSerialize $serialize
  276.     ): Response {
  277.         if (!$execution) {
  278.             return $this->json([
  279.                 'message'  => $translator->trans('Task execution not found'),
  280.                 'severity' => SeverityInterface::ERROR,
  281.             ], Response::HTTP_NOT_FOUND);
  282.         }
  283.         try {
  284.             $materialRequests = $service->listByExecution($execution);
  285.         } catch (\DomainException $e) {
  286.             return $this->json([
  287.                 'message'  => $translator->trans($e->getMessage()),
  288.                 'severity' => SeverityInterface::ERROR,
  289.             ], Response::HTTP_CONFLICT);
  290.         }
  291.         return $this->json([
  292.             'message'          => $translator->trans('Material requests loaded'),
  293.             'severity'         => SeverityInterface::SUCCESS,
  294.             'materialRequests' => array_map(
  295.                 function (MaterialRequest $mr) use ($serialize) {
  296.                     return $serialize->setEntity($mr)->serializeFull();
  297.                 },
  298.                 $materialRequests
  299.             ),
  300.         ], Response::HTTP_OK);
  301.     }
  302.     /**
  303.      * INCELEYEN GELEN KUTUSU — karar bekleyen talepler (ExpenseV2 toolbar'indaki
  304.      * "Malzeme talepleri" butonu bunu okur).
  305.      *
  306.      * 🪤 BU UC OLMADAN AKIS `submitted`'DA OLUYORDU: gonderilen talep hicbir yere
  307.      * dusmuyor, tek okuma ucu "execution'a gore listele" idi — yani inceleyenin
  308.      * once HANGI execution oldugunu bilmesi gerekiyordu. Bilmesinin bir yolu yoktu.
  309.      *
  310.      * Tek dataset -> duz `HttpRequest` (streamed masterData DEGIL), `methods`
  311.      * kisitlanmiyor (GET calisir).
  312.      *
  313.      * @Route("/list-pending", name="api_v3_material_request_list_pending")
  314.      * @IsGranted("MODULE:expense:u")
  315.      */
  316.     public function listPending(
  317.         TranslatorInterface $translator,
  318.         MaterialRequestService $service,
  319.         MaterialRequestSerialize $serialize
  320.     ): Response {
  321.         $materialRequests = $service->listPending();
  322.         return $this->json([
  323.             'message'          => $translator->trans('Material requests loaded'),
  324.             'severity'         => SeverityInterface::SUCCESS,
  325.             'materialRequests' => array_map(
  326.                 function (MaterialRequest $mr) use ($serialize) {
  327.                     // Gelen kutusu task/order kunyesini de ister — bkz. serializeForInbox.
  328.                     return $serialize->setEntity($mr)->serializeForInbox();
  329.                 },
  330.                 $materialRequests
  331.             ),
  332.         ], Response::HTTP_OK);
  333.     }
  334.     /**
  335.      * Wizard'in ihtiyac duydugu tek koleksiyon: item_groups (kategori secimi).
  336.      *
  337.      * ⚠️ TEK dataset -> CLAUDE.md "tek dataset -> HttpRequest (normal JSON)" kurali:
  338.      * StreamedResponse DEGIL, duz Response. FE (te-frontend) bu ucu `HttpRequest(...,
  339.      * "GET")` ile cagirip `data.item_groups` okuyor — StreamedResponse (SSE metni) FE'de
  340.      * parse edilemezdi. Rol model: TaskExecutionController::neededCollections 3 koleksiyon
  341.      * bundle ettigi icin StreamedResponse'du; burada tek koleksiyon oldugu icin farkli.
  342.      *
  343.      * @Route("/needed-collections", name="api_v3_material_request_needed_collections")
  344.      * @IsGranted("SECTION:order:task:u")
  345.      */
  346.     public function neededCollections(
  347.         MaterialRequestService $service,
  348.         TranslatorInterface $translator,
  349.         ItemGroupsSerialize $itemGroupsSerialize
  350.     ): Response {
  351.         return $this->json([
  352.             'message'     => $translator->trans('Item groups loaded'),
  353.             'severity'    => SeverityInterface::SUCCESS,
  354.             'item_groups' => $itemGroupsSerialize->setEntity($service->fetchAllItemGroups())->serializeFull(),
  355.         ], Response::HTTP_OK);
  356.     }
  357. }