src/Controller/ClientSidePublisherController.php line 122

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Config;
  4. use App\Entity\MafoId\MafoAffiliates;
  5. use App\Entity\MafoId\MafoOffers;
  6. use App\Entity\Tune\AffiliateInfo;
  7. use App\Entity\Employees;
  8. use App\Entity\AgentControl;
  9. use App\Entity\MmpMobileApps;
  10. use App\Entity\MmpOffers;
  11. use App\Services\AffiliateHasofferAPI;
  12. use App\Services\Alerts;
  13. use App\Services\Aws\ElasticCache;
  14. use App\Services\Aws\S3;
  15. use App\Services\BrandHasofferAPI;
  16. use App\Services\Common;
  17. use App\Services\MafoFinancialToolsComponents;
  18. use App\Services\ImpressionsApis;
  19. use App\Services\Metrics24APICalls;
  20. use App\Services\MmpComponents;
  21. use App\Services\MysqlQueries;
  22. use App\Services\UsersComponents;
  23. use Doctrine\Persistence\ManagerRegistry;
  24. use Mmoreram\GearmanBundle\Service\GearmanClientInterface;
  25. use Symfony\Component\Routing\Annotation\Route;
  26. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  27. use Symfony\Component\HttpFoundation\JsonResponse;
  28. use Symfony\Component\HttpFoundation\Request;
  29. use App\Repository\MafoPublisherCabinetManagerMappingWithAffiliateRepository;
  30. use App\Entity\MafoPublisherCabinetInvoice;
  31. use Symfony\Component\HttpFoundation\Response;
  32. use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
  33. use function GuzzleHttp\json_encode;
  34. use App\Traits\TrafficReportTrait;
  35. /**
  36.  *
  37.  * Offer related routes with endpoint /api/offers/{route}
  38.  *
  39.  * @Route("/api/client/publisher", name="client_publisher_", host="%publishers_subdomain%")
  40.  */
  41. class ClientSidePublisherController extends AbstractController
  42. {
  43.     use TrafficReportTrait;
  44.     private $commonCalls;
  45.     private $doctrine;
  46.     private $mysqlQueries;
  47.     private $mafoFinancialToolsComponents;
  48.     private $alerts;
  49.     private $brandHasofferApi;
  50.     private $mmpComponents;
  51.     private $affiliateHasofferAPI;
  52.     private $usersComponents;
  53.     private $elasticCache;
  54.     private $projectDir;
  55.     private $s3;
  56.     private $impressionsApis;
  57.     private $metrics24APICalls;
  58.     private $gearmanClientInterface;
  59.     private $mappingRepository;
  60.     public function __construct(
  61.         Common                                                    $commonCalls,
  62.         ManagerRegistry                                           $doctrine,
  63.         MysqlQueries                                              $mysqlQueries,
  64.         MafoPublisherCabinetManagerMappingWithAffiliateRepository $mappingRepository,
  65.         MafoFinancialToolsComponents                              $mafoFinancialToolsComponents,
  66.         Alerts                                                    $alerts,
  67.         BrandHasofferApi                                          $brandHasofferApi,
  68.         MmpComponents                                             $mmpComponents,
  69.         AffiliateHasofferAPI                                      $affiliateHasofferAPI,
  70.         UsersComponents                                           $usersComponents,
  71.         ElasticCache                                              $elasticCache,
  72.         S3                                                        $s3,
  73.         ImpressionsApis                                           $impressionsApis,
  74.         Metrics24APICalls                                         $metrics24APICalls,
  75.         GearmanClientInterface                                    $gearmanClientInterface,
  76.         string                                                    $projectDir
  77.     ) {
  78.         $this->commonCalls $commonCalls;
  79.         $this->doctrine $doctrine;
  80.         $this->mysqlQueries $mysqlQueries;
  81.         $this->mafoFinancialToolsComponents $mafoFinancialToolsComponents;
  82.         $this->alerts $alerts;
  83.         $this->brandHasofferApi $brandHasofferApi;
  84.         $this->mmpComponents $mmpComponents;
  85.         $this->affiliateHasofferAPI $affiliateHasofferAPI;
  86.         $this->usersComponents $usersComponents;
  87.         $this->elasticCache $elasticCache;
  88.         $this->s3 $s3;
  89.         $this->impressionsApis $impressionsApis;
  90.         $this->metrics24APICalls $metrics24APICalls;
  91.         $this->gearmanClientInterface $gearmanClientInterface;
  92.         $this->projectDir $projectDir;
  93.         $this->mappingRepository $mappingRepository;
  94.     }
  95.     /**
  96.      * @Route("/validate", name="validate", methods={"GET"})
  97.      */
  98.     public function getValidateAction(Request $request)
  99.     {
  100.         return new JsonResponse(true);
  101.     }
  102.     /**
  103.      * @Route("/test", name="test", methods={"GET"})
  104.      */
  105.     public function getAgentControlAction(Request $request)
  106.     {
  107.         return new JsonResponse(true);
  108.     }
  109.     /**
  110.      * @Route("/login", name="login")
  111.      */
  112.     public function indexAction(AuthenticationUtils $authenticationUtils)
  113.     {
  114.         //        $authenticationUtils = $authenticationUtils->get('security.authentication_utils');
  115.         // get the login error if there is one
  116.         $error $authenticationUtils->getLastAuthenticationError();
  117.         // last username entered by the user
  118.         $lastUsername $authenticationUtils->getLastUsername();
  119.         return $this->render('/publisher/login/index.html.twig', [
  120.             'last_username' => $lastUsername,
  121.             'error' => $error,
  122.         ]);
  123.     }
  124.     /**
  125.      * @Route("/details", name="get_publisher_details",  methods={"GET"})
  126.      */
  127.     public function getPublishersDetails(Request $request): Response
  128.     {
  129.         $publisherInfo $this->getUser();
  130.         $publisherId $publisherInfo->getId();
  131.         $mappedAffiliateIds $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
  132.         $publisherData = [
  133.             'id' => $publisherInfo->getId(),
  134.             'email' => $publisherInfo->getEmail(),
  135.             'firstName' => $publisherInfo->getFirstName(),
  136.             'lastName' => $publisherInfo->getLastName(),
  137.             'status' => $publisherInfo->getStatus(),
  138.             'lastLoginAt' => $publisherInfo->getLastLoginAt(),
  139.             'dateUpdated' => $publisherInfo->getDateUpdated(),
  140.         ];
  141.         return $this->json($publisherData);
  142.     }
  143.     /**
  144.      * @Route("/report-columns/{report}", name="get_report_columns", methods={"GET"})
  145.      */
  146.     public function getReportColumnsAction(Request $request$report)
  147.     {
  148.         if (in_array($reportarray_keys(Config::TABLE_COLUMNS_WITH_JSON_FILE))) {
  149.             return new JsonResponse(array_values($this->commonCalls->getDataFromJsonFile(Config::TABLE_COLUMNS_WITH_JSON_FILE[$report])));
  150.         }
  151.     }
  152.     /**
  153.      * @Route("/hyper-statuses", name="get_hyper_statuses", methods={"GET"})
  154.      */
  155.     public function getHyperStatusesAction(Request $request)
  156.     {
  157.         $arr = [];
  158.         foreach (array_values(Config::HYPER_STATUS_MAFO_MACROS) as $key => $value) {
  159.             $arr[$value] = [
  160.                 'value' => $value,
  161.                 'label' => $value,
  162.             ];
  163.         }
  164.         return new JsonResponse(array_values($arr));
  165.     }
  166.     /**
  167.      * @Route("/affiliates", name="get_affiliates", methods={"GET"})
  168.      */
  169.     public function getAffiliatesAction(Request $request)
  170.     {
  171.         $publisherInfo $this->getUser();
  172.         $publisherId $publisherInfo->getId();
  173.         $mappedAffiliateIds $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
  174.         $keyword $request->query->get('keyword');
  175.         $dataByKeyword = [];
  176.         if ($keyword) {
  177.             $dataByKeyword $this->doctrine->getRepository(AffiliateInfo::class)->getAffiliateByKeyword($keyword);
  178.         }
  179.         $statusArr $request->query->get('status') != '' $request->query->get('status') : [Config::ACTIVE_STATUS];
  180.         $affiliateData $this->commonCalls->getPublisherAffiliateListByStatusWithKeys($statusArr$mappedAffiliateIds);
  181.         $affiliateList = [];
  182.         foreach ($affiliateData as $key => $value) {
  183.             $affiliateList[$value['id']] = [
  184.                 'value' => $value['id'],
  185.                 'label' => $value['id'] . ' - ' $value['name'],
  186.                 'status' => $value['status']
  187.             ];
  188.         }
  189.         foreach ($dataByKeyword as $key => $value) {
  190.             if (!array_key_exists($value['affiliateId'], $affiliateList)) {
  191.                 $temp = [
  192.                     'value' => (int)$value['affiliateId'],
  193.                     'label' => $value['affiliateId'] . ' - ' $value['company'],
  194.                     'status' => $value['status']
  195.                 ];
  196.                 $affiliateList[$value['affiliateId']] = $temp;
  197.             }
  198.         }
  199.         ksort($affiliateList);
  200.         return new JsonResponse(array_values($affiliateList));
  201.     }
  202.     /**
  203.      * @Route("/mafo-users", name="get_mafo_users", methods={"GET"})
  204.      */
  205.     public function getAffiliateManagersAction(Request $request)
  206.     {
  207.         $affiliateData $this->getAffiliateAndManagerData();
  208.         $mappedAffiliateIds $affiliateData['mappedAffiliateIds'];
  209.         if (empty($mappedAffiliateIds)) {
  210.             return new JsonResponse([]);
  211.         }
  212.         $accountManagerEmails $this->doctrine->getRepository(MafoAffiliates::class)
  213.             ->getAccountManagerEmailsByAffiliateIds($mappedAffiliateIds);
  214.         if (empty($accountManagerEmails)) {
  215.             return new JsonResponse([]);
  216.         }
  217.         $employeesInfo $this->doctrine->getRepository(Employees::class)
  218.             ->getEmployeesByEmails($accountManagerEmails);
  219.         $responseArr = [];
  220.         foreach ($employeesInfo as $email => $employee) {
  221.             $responseArr[] = [
  222.                 'value' => $employee['email'] ?? '',
  223.                 'label' => $employee['fullName'] . "[{$employee['email']}]"
  224.             ];
  225.         }
  226.         usort($responseArr, fn($a$b) => strcmp($a['label'], $b['label']));
  227.         return new JsonResponse($responseArr);
  228.     }
  229.     private function getAffiliateAndManagerData()
  230.     {
  231.         $publisherInfo $this->getUser();
  232.         $publisherId $publisherInfo->getId();
  233.         $mappedAffiliateIds $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
  234.         $accountManagerEmails $this->doctrine->getRepository(MafoAffiliates::class)
  235.             ->getAccountManagerEmailsByAffiliateIds($mappedAffiliateIds);
  236.         $mappedAffiliateIds array_map('strval'$mappedAffiliateIds);
  237.         return [
  238.             'formattedArray' => [
  239.                 'MULTISELECT_MAFO_AFFILIATES' => $mappedAffiliateIds,
  240.             ],
  241.             'formattedTrafficReportArray' => [
  242.                 'MULTISELECT_MAFO_AFFILIATES' => $mappedAffiliateIds,
  243.             ],
  244.             'mappedAffiliateIds' => $mappedAffiliateIds,
  245.             'accountManagerEmails' => $accountManagerEmails,
  246.         ];
  247.     }
  248.     /**
  249.      * @Route("/financial-report", name="get_financial_report", methods={"GET"})
  250.      */
  251.     public function getfinancialReportAction(Request $request)
  252.     {
  253.         $affiliateData $this->getAffiliateAndManagerData();
  254.         $formattedArray $affiliateData['formattedArray'];
  255.         $mappedAffiliateIds $affiliateData['mappedAffiliateIds'];
  256.         $mappedAccountManagerEmails $affiliateData['accountManagerEmails'];
  257.         if (empty($mappedAffiliateIds)) {
  258.             return new JsonResponse([
  259.                 'response' => [
  260.                     'success' => true,
  261.                     'httpStatus' => Config::HTTP_STATUS_CODE_OK,
  262.                     'data' => [],
  263.                     'message' => 'No data available.',
  264.                     'error' => null
  265.                 ]
  266.             ], Config::HTTP_STATUS_CODE_OK);
  267.         }
  268.         $selectedColumns $request->query->all('data') != '' $request->query->all('data') : [];
  269.         $validationError $this->validatePublisherFinancialReportFields($selectedColumns);
  270.         if ($validationError !== null) {
  271.             return new JsonResponse($validationErrorConfig::HTTP_STATUS_CODE_BAD_REQUEST);
  272.         }
  273.         $filtersSelected $request->query->all('filters') ?? [];
  274.         $excludedFlagForFilters $request->query->all('excludedFlagForFilters') ?? [];
  275.         $processedFilters $this->processPublisherFinancialReportFilters(
  276.             $filtersSelected,
  277.             $excludedFlagForFilters,
  278.             $mappedAffiliateIds
  279.         );
  280.         $filters $processedFilters['filters'];
  281.         $excludedFiltersFlags $processedFilters['excludedFiltersFlags'];
  282.         $limit $request->query->get('limit') ? $request->query->get('limit') : Config::REPORTS_PAGINATION_DEFAULT_PAGE_SIZE;
  283.         $page $request->query->get('page') ? $request->query->get('page') : Config::REPORTS_PAGINATION_DEFAULT_PAGE_NUMBER;
  284.         $sortBy $request->query->get('sortBy') ?? Config::REPORTS_PAGINATION_DEFAULT_SORT_BY;
  285.         $sortType $request->query->get('sortType') ?? Config::REPORTS_PAGINATION_DEFAULT_SORT_TYPE;
  286.         $dateStart date('Y-m-1'strtotime('-0 month'strtotime($request->query->get('startDate'))));
  287.         $dateEnd date('Y-m-1'strtotime('+0 month 'strtotime($request->query->get('endDate'))));
  288.         if ($request->query->get('downloadCSV') == 'true') {
  289.             $limit Config::REPORTS_PAGINATION_DEFAULT_CSV_PAGE_SIZE;
  290.             $page Config::REPORTS_PAGINATION_DEFAULT_PAGE_NUMBER;
  291.         }
  292.         $finalArr $this->mafoFinancialToolsComponents->getPayoutTotalAggregatedData($dateStart$dateEnd$filters$excludedFiltersFlags$selectedColumnsnull);
  293.         $allowedStatuses array_values(Config::HYPER_STATUS_AFFILIATE_MAFO_MACROS);
  294.         foreach ($finalArr as &$record) {
  295.             if (!in_array($record['hyperStatus'], $allowedStatuses)) {
  296.                 $record['hyperStatus'] = '';
  297.             }
  298.         }
  299.         $tableColumns $this->commonCalls->changeColumnVisibilityForTable(array_values($this->commonCalls->getDataFromJsonFile(Config::JSON_FILE_CLIENT_SIDE_PUBLIHSER_FINANCIAL_REPORT)), $selectedColumns, []);
  300.         if ($request->query->get('downloadCSV') == 'true') {
  301.             $this->commonCalls->downloadCSV($tableColumns$finalArr'Financial Report ' $dateStart '_' $dateEnd);
  302.         }
  303.         return new JsonResponse($this->commonCalls->getReportResponse($finalArr$tableColumns$limit$page$sortBy$sortType));
  304.     }
  305.     /**
  306.      * @Route("/traffic-report", methods={"GET"})
  307.      */
  308.     public function getTrafficReport(Request $request)
  309.     {
  310.         $affiliateData $this->getAffiliateAndManagerData();
  311.         $formattedTrafficReportArray $affiliateData['formattedTrafficReportArray'];
  312.         $mappedAffiliateIds $affiliateData['mappedAffiliateIds'];
  313.         if (empty($mappedAffiliateIds)) {
  314.             return new JsonResponse([
  315.                 'response' => [
  316.                     'success' => true,
  317.                     'httpStatus' => Config::HTTP_STATUS_CODE_OK,
  318.                     'data' => [],
  319.                     'message' => 'No data available.',
  320.                     'error' => null
  321.                 ]
  322.             ], Config::HTTP_STATUS_CODE_OK);
  323.         }
  324.         $selectedColumns $request->query->all('data') != '' $request->query->all('data') : [];
  325.         $validationError $this->validatePublisherTrafficReportFields($selectedColumns);
  326.         if ($validationError !== null) {
  327.             return new JsonResponse($validationErrorConfig::HTTP_STATUS_CODE_BAD_REQUEST);
  328.         }
  329.         $filtersSelected $request->query->all('filters') ?? [];
  330.         $excludedFlagForFilters $request->query->all('excludedFlagForFilters') ?? [];
  331.         $processedFilters $this->processPublisherTrafficReportFilters(
  332.             $filtersSelected,
  333.             $excludedFlagForFilters,
  334.             $mappedAffiliateIds
  335.         );
  336.         $filters $processedFilters['filters'];
  337.         $excludedFiltersFlags $processedFilters['excludedFiltersFlags'];
  338.         $selectedColumns $request->query->all('data') != '' $request->query->all('data') : [];
  339.         $groupedColumns $request->query->all('groups') != '' $request->query->all('groups') : [];
  340.         $dateStart date('Y-m-d'strtotime($request->query->get('startDate')));
  341.         $dateEnd date('Y-m-d'strtotime($request->query->get('endDate')));
  342.         $eventTimestampFrom strtotime($dateStart);
  343.         $eventTimeStampTo strtotime($dateEnd);
  344.         $sortBy $request->query->get('sortBy') ?? Config::REPORTS_PAGINATION_MMP_REPORT_DEFAULT_SORT_BY;
  345.         $sortType $request->query->get('sortType') ?? Config::REPORTS_PAGINATION_DEFAULT_SORT_TYPE;
  346.         $page $request->query->get('page') ?? Config::REPORTS_PAGINATION_DEFAULT_PAGE_NUMBER;
  347.         $limit $request->query->get('limit') ?? Config::REPORTS_PAGINATION_DEFAULT_PAGE_SIZE;
  348.         $downloadDataAsCSV $request->query->get('downloadCSV') == 'true';
  349.         $tableColumns $this->commonCalls->changeColumnVisibilityForTable(
  350.             array_values($this->commonCalls->getDataFromJsonFile(Config::JSON_FILE_CLIENT_SIDE_PUBLISHER_TRAFFIC_REPORT)),
  351.             $selectedColumns,
  352.             []
  353.         );
  354.         $finalReportData $this->mmpComponents->getMmpCumulativeData(
  355.             $filters,
  356.             $excludedFiltersFlags,
  357.             $eventTimestampFrom,
  358.             $eventTimeStampTo,
  359.             $selectedColumns,
  360.             $groupedColumns
  361.         );
  362.         $finalReportData $this->mmpComponents->normalizeMmpSource($finalReportData);
  363.         foreach ($tableColumns as $key => $value) {
  364.             if (isset($value['aggregate']) && $value['aggregate'] == 'sum') {
  365.                 $num round(array_sum(array_column($finalReportData$value['accessor'])), 2);
  366.                 if ($value['category'] == 'statistics') {
  367.                     $tableColumns[$key]['Footer'] = number_format($num);
  368.                 } else {
  369.                     $tableColumns[$key]['Footer'] = number_format($num2);
  370.                 }
  371.             }
  372.         }
  373.         if ($downloadDataAsCSV) {
  374.             $this->commonCalls->downloadCSV($tableColumns$finalReportData'Traffic Report ' $dateStart '_' $dateEnd);
  375.         } else {
  376.             $finalReportData array_values($finalReportData);
  377.             if (sizeof($finalReportData)) {
  378.                 $entity $finalReportData[0];
  379.                 if (array_key_exists($sortBy$entity)) {
  380.                     $sortFlag 3;
  381.                     if ($sortType == Config::SORT_TYPE_ASC) {
  382.                         $sortFlag 4;
  383.                     }
  384.                     array_multisort(array_column($finalReportData$sortBy), $sortFlag$finalReportData);
  385.                 }
  386.             }
  387.             $offset $limit * ($page 1);
  388.             $totalRecordCount sizeof($finalReportData);
  389.             $noOfPages ceil($totalRecordCount $limit);
  390.             return new JsonResponse([
  391.                 'response' => [
  392.                     'success' => true,
  393.                     'httpStatus' => Config::HTTP_STATUS_CODE_OK,
  394.                     'data' => [
  395.                         'tableColumns' => $tableColumns,
  396.                         'data' => array_slice($finalReportData$offset$limit),
  397.                         'metaData' => [
  398.                             'total' => $totalRecordCount,
  399.                             'limit' => (int)$limit,
  400.                             'page' => (int)$page,
  401.                             'pages' => (int)$noOfPages,
  402.                         ]
  403.                     ],
  404.                     'error' => null
  405.                 ]
  406.             ], Config::HTTP_STATUS_CODE_OK);
  407.         }
  408.     }
  409.     /**
  410.      * @Route("/table-columns/{table}", name="get_table_columns", methods={"GET"})
  411.      */
  412.     public function getTableColumnsAction(Request $request$table)
  413.     {
  414.         return new JsonResponse(array_values($this->commonCalls->getDataFromJsonFile(Config::TABLE_COLUMNS_WITH_JSON_FILE[$table])));
  415.     }
  416.     /**
  417.      * @Route("/mobile-apps-list", name="get_mobile_apps_list", methods={"GET"})
  418.      */
  419.     public function getMobileAppsListAction(Request $request)
  420.     {
  421.         $mobileAppsData $this->doctrine->getRepository(MmpMobileApps::class)->getMmpMobileAppByIsDeleted(0);
  422.         $appIds array_values(array_unique(array_merge(array_column($mobileAppsData'bundleId'), [])));
  423.         $appData $this->commonCalls->getAppInfoWithKeys();
  424.         $data = [];
  425.         foreach ($appIds as $key => $value) {
  426.             if (preg_match("/\t/"$value)) {
  427.                 continue;
  428.             }
  429.             $appName array_key_exists($value$appData) ? " - " $appData[$value]['title'] : '';
  430.             $data[$value] = [
  431.                 'value' => $value,
  432.                 'label' => $value $appName
  433.             ];
  434.         }
  435.         return new JsonResponse(array_values($data));
  436.     }
  437.     /**
  438.      * @Route("/countries", name="get_countries", methods={"GET"})
  439.      */
  440.     public function getCountriesAction()
  441.     {
  442.         $countryList = [];
  443.         foreach (Config::COUNTRIES as $key => $value) {
  444.             $countryList[$key] = [
  445.                 'value' => $key,
  446.                 'label' => $key ' - ' $value['name']
  447.             ];
  448.         }
  449.         ksort($countryList);
  450.         return new JsonResponse(array_values($countryList));
  451.     }
  452.     /**
  453.      * @Route("/mafo-affiliates", methods={"GET"})
  454.      */
  455.     public
  456.     function getMafoAffiliatesAction(Request $request)
  457.     {
  458.         $affiliateData $this->getAffiliateAndManagerData();
  459.         $mappedAffiliateIds $affiliateData['mappedAffiliateIds'];
  460.         if (empty($mappedAffiliateIds)) {
  461.             return new JsonResponse([]);
  462.         }
  463.         $affiliates $this->doctrine->getRepository(MafoAffiliates::class)->getAffiliates([], [], Config::REPORTS_PAGINATION_DEFAULT_CSV_PAGE_SIZEConfig::REPORTS_PAGINATION_DEFAULT_PAGE_NUMBERConfig::REPORTS_PAGINATION_DEFAULT_SORT_BYConfig::REPORTS_PAGINATION_DEFAULT_SORT_TYPE);
  464.         $arr = [];
  465.         foreach ($affiliates as $key => $value) {
  466.             if (in_array((string)$value['id'], $mappedAffiliateIds)) {
  467.                 $arr[] = [
  468.                     'value' => $value['id'],
  469.                     'label' => $value['id'] . ' - ' $value['name']
  470.                 ];
  471.             }
  472.         }
  473.         return new JsonResponse($arr);
  474.     }
  475.     /**
  476.      * @Route("/offers", name="get_offers", methods={"GET"})
  477.      */
  478.     public function getPublisherOffersAction(Request $request)
  479.     {
  480.         $filters $request->query->all('filters') ?? [];
  481.         $excludedFlagForFilters $request->query->all('excludedFlagForFilters') ?? []; 
  482.         $publisherInfo $this->getUser();
  483.         $publisherId $publisherInfo->getId();
  484.         $limit $request->query->get('limit') ? $request->query->get('limit') : Config::REPORTS_PAGINATION_DEFAULT_PAGE_SIZE;
  485.         $page $request->query->get('page') ? $request->query->get('page') : Config::REPORTS_PAGINATION_DEFAULT_PAGE_NUMBER;
  486.         $mmpOfferIds $appIds $mafoAdvertiserIds $geos = [];
  487.         if (isset($filters)) {
  488.             foreach ($filters as $key => $value) {
  489.                 $key === 'MULTISELECT_MMP_OFFERS' $mmpOfferIds $value false;
  490.                 $key === 'MULTISELECT_MMP_STATISTICS_APP' $appIds $value false;
  491.                 $key === 'MULTISELECT_MAFO_ADVERTISERS' $mafoAdvertiserIds $value false;
  492.                 $key === 'MULTISELECT_GEO' $geos $value false;
  493.             }
  494.         }
  495.         $mmpOfferIdsExcluded $appIdsExcluded $mafoAdvertiserIdsExcluded $excludeGeos = [];
  496.         if (isset($excludedFlagForFilters)) {
  497.             foreach ($excludedFlagForFilters as $key => $value) {
  498.                 $key === 'MULTISELECT_MMP_OFFERS' $mmpOfferIdsExcluded $value false;
  499.                 $key === 'MULTISELECT_MMP_STATISTICS_APP' $appIdsExcluded $value false;
  500.                 $key === 'MULTISELECT_MAFO_ADVERTISERS' $mafoAdvertiserIdsExcluded $value false;
  501.                 $key === 'MULTISELECT_GEO' $excludeGeos $value false;
  502.             }
  503.         }
  504.         $selectedColumns $request->query->all('data') != '' $request->query->all('data') : [];
  505.         $publisherOfferIds $this->mmpComponents->getVisibleOfferIdsForPublisherManager($publisherId);
  506.         $mappedAffiliateIds $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
  507.         // If no offers found for this publisher, return empty result
  508.         if (empty($publisherOfferIds)) {
  509.             return new JsonResponse([
  510.                 'response' => [
  511.                     'success' => true,
  512.                     'httpStatus' => Config::HTTP_STATUS_CODE_OK,
  513.                     'data' => [
  514.                         'tableColumns' => [],
  515.                         'data' => [],
  516.                         'metaData' => [
  517.                             'total' => 0,
  518.                             'limit' => (int)$limit,
  519.                             'page' => (int)$page,
  520.                             'pages' => 0,
  521.                         ]
  522.                     ],
  523.                     'error' => null
  524.                 ]
  525.             ], Config::HTTP_STATUS_CODE_OK);
  526.         }
  527.         // Merge with existing mmpOfferIds filter
  528.         if (!empty($mmpOfferIds)) {
  529.             $mmpOfferIds array_intersect($mmpOfferIds$publisherOfferIds);
  530.         } else {
  531.             $mmpOfferIds $publisherOfferIds;
  532.         }
  533.         // Use the common service to get and process offers data
  534.         $filters = [
  535.             'MULTISELECT_MMP_OFFERS' => $mmpOfferIds,
  536.             'MULTISELECT_MMP_STATISTICS_APP' => $appIds,
  537.             'MULTISELECT_MAFO_ADVERTISERS' => $mafoAdvertiserIds,
  538.             'MULTISELECT_GEO' => $geos,
  539.         ];
  540.         $excludedFlagForFilters = [
  541.             'MULTISELECT_MMP_OFFERS' => $mmpOfferIdsExcluded,
  542.             'MULTISELECT_MMP_STATISTICS_APP' => $appIdsExcluded,
  543.             'MULTISELECT_MAFO_ADVERTISERS' => $mafoAdvertiserIdsExcluded,
  544.             'MULTISELECT_GEO' => $excludeGeos,
  545.         ];
  546.         $offersData $this->mmpComponents->getMmpOffersData($filters$excludedFlagForFilters);
  547.         $hoAffiliateListList $this->commonCalls->getAffiliateListByStatusWithKeys();
  548.         $processedData $this->mmpComponents->processOffersData($offersData$hoAffiliateListList);
  549.         $offersData $processedData['offersData'];
  550.         $publisherPermissionsByOfferId $processedData['publisherPermissionsByOfferId'];
  551.         $tableColumns $this->commonCalls->changeColumnVisibilityForTable(array_values($this->commonCalls->getDataFromJsonFile(Config::JSON_FILE_CLIENT_SIDE_PUBLIHSER_MMP_STATISTICS_MMP_OFFERS)), $selectedColumns, []);
  552.         // Pass publisher manager ID to filter allowed/blocked lists to show only managed affiliates
  553.         $finalArr $this->mmpComponents->formatOffersForResponse($offersData$publisherPermissionsByOfferId$publisherId);
  554.         $finalArr $this->mmpComponents->normalizeMmpSource($finalArr);
  555.         $finalArr $this->replacePayoutFromPartnerRules($finalArr$selectedColumns$mappedAffiliateIds);
  556.         $downloadCSV $request->query->get('downloadCSV');
  557.         if ($downloadCSV == || $downloadCSV === 'true' || $downloadCSV === true) {
  558.             $this->commonCalls->downloadCSV($tableColumns$finalArr'MMP Offers');
  559.         }
  560.         $offset $limit * ($page 1);
  561.         $totalRecordCount sizeof($finalArr);
  562.         $noOfPages ceil($totalRecordCount $limit);
  563.         return new JsonResponse([
  564.             'response' => [
  565.                 'success' => true,
  566.                 'httpStatus' => Config::HTTP_STATUS_CODE_OK,
  567.                 'data' => [
  568.                     'tableColumns' => $tableColumns,
  569.                     'data' => array_slice($finalArr$offset$limit),
  570.                     'metaData' => [
  571.                         'total' => $totalRecordCount,
  572.                         'limit' => (int)$limit,
  573.                         'page' => (int)$page,
  574.                         'pages' => (int)$noOfPages,
  575.                     ]
  576.                 ],
  577.                 'error' => null
  578.             ]
  579.         ], Config::HTTP_STATUS_CODE_OK);
  580.     }
  581.     /**
  582.      * Get visible offers for a Publisher Manager based on new affiliate mapping logic
  583.      * @param int $publisherManagerId The Publisher Manager (PAM) ID
  584.      * @return array Array of visible offer IDs
  585.      */
  586.     /**
  587.      * @Route("/mmp-tracking-system", name="get_mmp-tracking-system", methods={"GET"})
  588.      */
  589.     public function getMmpTrackingSystem(Request $request)
  590.     {
  591.         $mmpTrackingSystem = [];
  592.         foreach (Config::MMP_TRACKING_SYSTEM_ADVERTISER_CABINET_SIMPLIFIED as $key => $value) {
  593.             $mmpTrackingSystem[] = [
  594.                 'value' => $key,
  595.                 'label' => $value
  596.             ];
  597.         }
  598.         return new JsonResponse($mmpTrackingSystem);
  599.     }
  600.     /**
  601.      * @Route("/mafo-offers", methods={"GET"})
  602.      */
  603.     public
  604.     function getMafoOffersAction(Request $request)
  605.     {
  606.         $offers $this->doctrine->getRepository(MafoOffers::class)->getOffersList();
  607.         $arr = [];
  608.         foreach ($offers as $key => $value) {
  609.             $arr[] = [
  610.                 'value' => $value['id'],
  611.                 'label' => $value['id'] . ' - ' $value['name']
  612.             ];
  613.         }
  614.         return new JsonResponse($arr);
  615.     }
  616.     /**
  617.      * @Route("/advertiser-manager", name="get_advertiser_managers", methods={"GET"})
  618.      */
  619.     public function getMafoUsersAction(Request $request)
  620.     {
  621.         // Forward to UtilitiesController logic
  622.         return $this->forward('App\Controller\UtilitiesController::getMafoUsersAction', [
  623.             'request' => $request,
  624.         ]);
  625.     }
  626.     /**
  627.      * Replace payout and payoutModelPretty values from mmp_partner_rules table
  628.      *
  629.      * @param array $finalArr - The formatted offers array
  630.      * @param array $selectedColumns - The columns selected by the user
  631.      * @param array $mappedAffiliateIds - The affiliate IDs mapped to the publisher
  632.      * @return array - The modified offers array with replaced payout values
  633.      */
  634.     private function replacePayoutFromPartnerRules(array $finalArr, array $selectedColumns, array $mappedAffiliateIds): array
  635.     {
  636.         if (empty($finalArr)) {
  637.             return $finalArr;
  638.         }
  639.         $mmpOfferIds array_unique(array_column($finalArr'mmpOfferId'));
  640.         if (empty($mmpOfferIds)) {
  641.             return $finalArr;
  642.         }
  643.         $mmpPartnerRulesRepo $this->doctrine->getRepository(\App\Entity\MmpPartnerRules::class);
  644.         $partnerRules $mmpPartnerRulesRepo->getPayoutRulesByMmpOfferIds($mmpOfferIds);
  645.         if (empty($partnerRules)) {
  646.             return $finalArr;
  647.         }
  648.         $payoutMap = [];
  649.         foreach ($partnerRules as $rule) {
  650.             $mmpOfferId $rule['mmpOfferId'];
  651.             if (!isset($payoutMap[$mmpOfferId])) {
  652.                 $payoutMap[$mmpOfferId] = [
  653.                     'payout' => $rule['payout'],
  654.                     'payoutModel' => $rule['payoutModel']
  655.                 ];
  656.             }
  657.         }
  658.         foreach ($finalArr as &$offer) {
  659.             $mmpOfferId $offer['mmpOfferId'];
  660.             if (isset($payoutMap[$mmpOfferId])) {
  661.                 $partnerPayout $payoutMap[$mmpOfferId];
  662.                 if ($partnerPayout['payout'] !== null) {
  663.                     $offer['payout'] = $partnerPayout['payout'];
  664.                 }
  665.                 if ($partnerPayout['payoutModel'] !== null) {
  666.                     $offer['payoutModelPretty'] = strtoupper($partnerPayout['payoutModel']);
  667.                 }
  668.             }
  669.         }
  670.         return $finalArr;
  671.     }
  672.     // ==================== INVOICE CONTROL ENDPOINTS ====================
  673.     /**
  674.      * Get invoice payment statuses
  675.      * @Route("/invoice-control/statuses", name="get_invoice_statuses", methods={"GET"})
  676.      */
  677.     public function getInvoiceStatusesAction(Request $request)
  678.     {
  679.         return new JsonResponse($this->commonCalls->getPublisherCabinetInvoiceStatusOptions());
  680.     }
  681.     /**
  682.      * Get invoices list for the publisher cabinet
  683.      * Supports multiselect filters for status and affiliateId
  684.      * @Route("/invoice-control", name="get_invoices", methods={"GET"})
  685.      */
  686.     public function getInvoicesAction(Request $request)
  687.     {
  688.         $publisherId $this->getUser()->getId();
  689.         $mappedAffiliateIds $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
  690.         if (empty($mappedAffiliateIds)) {
  691.             return new JsonResponse([
  692.                 'response' => [
  693.                     'success' => true,
  694.                     'httpStatus' => Config::HTTP_STATUS_CODE_OK,
  695.                     'data' => ['data' => [], 'metaData' => ['total' => 0'limit' => Config::REPORTS_PAGINATION_DEFAULT_PAGE_SIZE'page' => 1'pages' => 0]],
  696.                     'message' => 'No data available.',
  697.                     'error' => null
  698.                 ]
  699.             ], Config::HTTP_STATUS_CODE_OK);
  700.         }
  701.         // Parse filters - support multiselect (arrays) for status and affiliateId
  702.         $filters = [];
  703.         
  704.         // Payment Status filter (multiselect): status[0]=open&status[1]=approved
  705.         $statusFilter $request->query->all('status');
  706.         if (!empty($statusFilter)) {
  707.             $filters['status'] = is_array($statusFilter) ? $statusFilter : [$statusFilter];
  708.         }
  709.         
  710.         // Mafo Publisher Id filter (multiselect): affiliateId[0]=55&affiliateId[1]=838
  711.         $affiliateIdFilter $request->query->all('affiliateId');
  712.         if (!empty($affiliateIdFilter)) {
  713.             // Only allow filtering by mapped affiliate IDs (security check)
  714.             $requestedIds is_array($affiliateIdFilter) ? $affiliateIdFilter : [$affiliateIdFilter];
  715.             $validIds array_intersect(array_map('intval'$requestedIds), array_map('intval'$mappedAffiliateIds));
  716.             if (!empty($validIds)) {
  717.                 $filters['mafoAffiliateId'] = $validIds;
  718.             }
  719.         }
  720.         // Date Range filter
  721.         $dateStart $request->query->get('startDate') ? date('Y-m-d'strtotime($request->query->get('startDate'))) : null;
  722.         $dateEnd $request->query->get('endDate') ? date('Y-m-d'strtotime($request->query->get('endDate'))) : null;
  723.         // Pagination
  724.         $limit = (int)($request->query->get('limit') ?: Config::REPORTS_PAGINATION_DEFAULT_PAGE_SIZE);
  725.         $page = (int)($request->query->get('page') ?: Config::REPORTS_PAGINATION_DEFAULT_PAGE_NUMBER);
  726.         
  727.         // Sorting - map created_at to dateInserted
  728.         $sortBy $request->query->get('sortBy') ?? 'id';
  729.         $sortType strtoupper($request->query->get('sortType') ?? Config::REPORTS_PAGINATION_DEFAULT_SORT_TYPE);
  730.         
  731.         // Map sort field names
  732.         $sortFieldMap = ['created_at' => 'dateInserted''updated_at' => 'dateUpdated'];
  733.         $sortBy $sortFieldMap[$sortBy] ?? $sortBy;
  734.         // Get invoices
  735.         $invoices $this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->getInvoicesByPublisherManagerId($publisherId$mappedAffiliateIds$dateStart$dateEnd$filters);
  736.         // Get affiliate names for display
  737.         $affiliateNames $this->getAffiliateNamesMap($mappedAffiliateIds);
  738.         // Format the data with canEdit flag based on status
  739.         $formattedInvoices array_map(function($invoice) use ($affiliateNames) {
  740.             $formatted $this->formatInvoiceForResponse($invoice$affiliateNames);
  741.             // Add canEdit flag - Edit button should be disabled if status is APPROVED
  742.             $formatted['canEdit'] = strtolower($invoice['status'] ?? '') !== Config::PUBLISHER_CABINET_INVOICE_STATUS_APPROVED;
  743.             return $formatted;
  744.         }, $invoices);
  745.         // Sorting
  746.         if (!empty($formattedInvoices) && isset($formattedInvoices[0][$sortBy])) {
  747.             $sortFlag = ($sortType === 'ASC') ? SORT_ASC SORT_DESC;
  748.             array_multisort(array_column($formattedInvoices$sortBy), $sortFlag$formattedInvoices);
  749.         }
  750.         // Pagination
  751.         $totalRecordCount count($formattedInvoices);
  752.         $noOfPages $totalRecordCount ceil($totalRecordCount $limit) : 0;
  753.         $offset $limit * ($page 1);
  754.         return new JsonResponse([
  755.             'response' => [
  756.                 'success' => true,
  757.                 'httpStatus' => Config::HTTP_STATUS_CODE_OK,
  758.                 'data' => [
  759.                     'data' => array_slice($formattedInvoices$offset$limit),
  760.                     'metaData' => ['total' => $totalRecordCount'limit' => $limit'page' => $page'pages' => (int)$noOfPages]
  761.                 ],
  762.                 'error' => null
  763.             ]
  764.         ], Config::HTTP_STATUS_CODE_OK);
  765.     }
  766.     /**
  767.      * Upload a new invoice
  768.      * @Route("/invoice-control", name="post_invoice", methods={"POST"})
  769.      */
  770.     public function postInvoiceAction(Request $request)
  771.     {
  772.         $publisherInfo $this->getUser();
  773.         $publisherId $publisherInfo->getId();
  774.         $mappedAffiliateIds $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
  775.         $errors = [];
  776.         
  777.         // Validate required fields
  778.         $mafoAffiliateId $request->request->get('mafoPartnerId');
  779.         $amount $request->request->get('amount');
  780.         $periodInput $request->request->get('period'); // Format: YYYY-MM (e.g., 2026-01)
  781.         $file $request->files->get('invoiceFile');
  782.         // Validation
  783.         if (empty($mafoAffiliateId)) {
  784.             $errors[] = 'MAFO Partner ID is required.';
  785.         } elseif (!in_array((int)$mafoAffiliateIdarray_map('intval'$mappedAffiliateIds))) {
  786.             $errors[] = 'Invalid MAFO Partner ID. You can only upload invoices for your mapped partners.';
  787.         }
  788.         if (empty($amount) || !is_numeric($amount) || $amount <= 0) {
  789.             $errors[] = 'Amount is required and must be a positive number.';
  790.         }
  791.         if (empty($periodInput)) {
  792.             $errors[] = 'Period (Year-Month) is required.';
  793.         } elseif (!preg_match('/^\d{4}-\d{2}$/'$periodInput)) {
  794.             $errors[] = 'Period must be in YYYY-MM format.';
  795.         }
  796.         if (!$file) {
  797.             $errors[] = 'Invoice file is required.';
  798.         } else {
  799.             // Validate file
  800.             $fileValidation $this->validateInvoiceFile($file);
  801.             if (!empty($fileValidation)) {
  802.                 $errors array_merge($errors$fileValidation);
  803.             }
  804.         }
  805.         if (!empty($errors)) {
  806.             return new JsonResponse([
  807.                 'response' => [
  808.                     'success' => false,
  809.                     'httpStatus' => Config::HTTP_STATUS_CODE_BAD_REQUEST,
  810.                     'data' => null,
  811.                     'error' => implode(' '$errors)
  812.                 ]
  813.             ], Config::HTTP_STATUS_CODE_BAD_REQUEST);
  814.         }
  815.         // Upload file to S3 (cabinet-uploads directory for GuardDuty scanning)
  816.         $s3Directory Config::S3_CABINET_UPLOADS_DIRECTORY;
  817.         $uploadDate = (new \DateTime())->format('Y-m-d');
  818.         $uploadTimestamp time();
  819.         $originalFileName $file->getClientOriginalName();
  820.         $linkToFile $s3Directory '/publisher/invoice-' $publisherId '-' $mafoAffiliateId '-' $uploadDate '-' $originalFileName '-' $uploadTimestamp;
  821.         
  822.         $uploadedFileKey $this->s3->uploadFileToS3($file->getPathname(), $linkToFile);
  823.         if (!$uploadedFileKey) {
  824.             return new JsonResponse([
  825.                 'response' => [
  826.                     'success' => false,
  827.                     'httpStatus' => 500,
  828.                     'data' => null,
  829.                     'error' => 'Failed to upload file. Please try again.'
  830.                 ]
  831.             ], 500);
  832.         }
  833.         // Create period datetime (first day of the month)
  834.         $period = new \DateTime($periodInput '-01');
  835.         // Create the invoice record
  836.         try {
  837.             $invoice $this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->createInvoice(
  838.                 $publisherId,
  839.                 (int)$mafoAffiliateId,
  840.                 $amount,
  841.                 $period,
  842.                 $uploadedFileKey,
  843.                 $request->request->get('currency') ?? Config::CURRENCY_USD
  844.             );
  845.             // Get affiliate name for response
  846.             $affiliateNames $this->getAffiliateNamesMap([(int)$mafoAffiliateId]);
  847.             $formattedInvoice $this->formatInvoiceForResponse([
  848.                 'id' => $invoice->getId(),
  849.                 'mafoPublisherCabinetManagerId' => $invoice->getMafoPublisherCabinetManagerId(),
  850.                 'mafoAffiliateId' => $invoice->getMafoAffiliateId(),
  851.                 'amount' => $invoice->getAmount(),
  852.                 'period' => $invoice->getPeriod(),
  853.                 'status' => $invoice->getStatus(),
  854.                 'linkToFile' => $invoice->getLinkToFile(),
  855.                 'currency' => $invoice->getCurrency(),
  856.                 'approvedByEmail' => $invoice->getApprovedByEmail(),
  857.                 'dateApproved' => $invoice->getDateApproved(),
  858.                 'dateUpdated' => $invoice->getDateUpdated(),
  859.                 'dateInserted' => $invoice->getDateInserted(),
  860.             ], $affiliateNames);
  861.             return new JsonResponse([
  862.                 'response' => [
  863.                     'success' => true,
  864.                     'httpStatus' => 201,
  865.                     'data' => $formattedInvoice,
  866.                     'message' => 'Invoice uploaded successfully.',
  867.                     'error' => null
  868.                 ]
  869.             ], 201);
  870.         } catch (\Exception $e) {
  871.             // Delete the uploaded file if invoice creation failed
  872.             $this->s3->deleteFileFromS3($uploadedFileKey);
  873.             return new JsonResponse([
  874.                 'response' => [
  875.                     'success' => false,
  876.                     'httpStatus' => 500,
  877.                     'data' => null,
  878.                     'error' => 'Failed to create invoice record. Please try again.'
  879.                 ]
  880.             ], 500);
  881.         }
  882.     }
  883.     /**
  884.      * Update an existing invoice (only amount and file editable, not for APPROVED status)
  885.      * @Route("/invoice-control/{id}", name="put_invoice", methods={"POST", "PUT", "PATCH"})
  886.      */
  887.     public function updateInvoiceAction(Request $requestint $id)
  888.     {
  889.         $publisherId $this->getUser()->getId();
  890.         $mappedAffiliateIds $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
  891.         $invoice $this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->getInvoiceByIdAndPublisher($id$publisherId$mappedAffiliateIds);
  892.         if (!$invoice) {
  893.             return new JsonResponse([
  894.                 'response' => ['success' => false'httpStatus' => 404'data' => null'error' => 'Invoice not found.']
  895.             ], 404);
  896.         }
  897.         if (!$this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->canEditInvoice($id)) {
  898.             return new JsonResponse([
  899.                 'response' => ['success' => false'httpStatus' => Config::HTTP_STATUS_CODE_FORBIDDEN'data' => null'error' => 'Cannot edit an approved invoice.']
  900.             ], Config::HTTP_STATUS_CODE_FORBIDDEN);
  901.         }
  902.         // Parse request data based on content type
  903.         $contentType $request->headers->get('Content-Type''');
  904.         $method $request->getMethod();
  905.         
  906.         if (strpos($contentType'application/json') !== false) {
  907.             $requestData json_decode($request->getContent(), true) ?? [];
  908.             $amount $requestData['amount'] ?? null;
  909.             $file null;
  910.         } elseif (strpos($contentType'multipart/form-data') !== false && in_array($method, ['PUT''PATCH'])) {
  911.             $parsedData $this->parseMultipartFormData($request);
  912.             $amount $parsedData['fields']['amount'] ?? null;
  913.             $file $parsedData['files']['invoiceFile'] ?? null;
  914.         } else {
  915.             $amount $request->request->get('amount');
  916.             $file $request->files->get('invoiceFile');
  917.         }
  918.         $errors = [];
  919.         $dataToUpdate = [];
  920.         // Validate amount
  921.         if ($amount !== null && $amount !== '') {
  922.             if (!is_numeric($amount) || $amount <= 0) {
  923.                 $errors[] = 'Amount must be a positive number.';
  924.             } else {
  925.                 $dataToUpdate['amount'] = $amount;
  926.             }
  927.         }
  928.         // Handle file update
  929.         if ($file) {
  930.             $fileValidation $this->validateInvoiceFile($file);
  931.             if (!empty($fileValidation)) {
  932.                 $errors array_merge($errors$fileValidation);
  933.             } else {
  934.                 if (!empty($invoice['linkToFile'])) {
  935.                     $this->s3->deleteFileFromS3($invoice['linkToFile']);
  936.                 }
  937.                 $filePath is_array($file) ? $file['tmp_name'] : $file->getPathname();
  938.                 $fileName is_array($file) ? $file['name'] : $file->getClientOriginalName();
  939.                 $fileKey Config::S3_CABINET_UPLOADS_DIRECTORY '/' $publisherId '/' time() . '-' $fileName;
  940.                 $uploadedFileKey $this->s3->uploadFileToS3($filePath$fileKey);
  941.                 
  942.                 if ($uploadedFileKey) {
  943.                     $dataToUpdate['linkToFile'] = $uploadedFileKey;
  944.                 } else {
  945.                     $errors[] = 'Failed to upload file.';
  946.                 }
  947.             }
  948.         }
  949.         if (!empty($errors)) {
  950.             return new JsonResponse([
  951.                 'response' => ['success' => false'httpStatus' => Config::HTTP_STATUS_CODE_BAD_REQUEST'data' => null'error' => implode(' '$errors)]
  952.             ], Config::HTTP_STATUS_CODE_BAD_REQUEST);
  953.         }
  954.         if (empty($dataToUpdate)) {
  955.             return new JsonResponse([
  956.                 'response' => ['success' => false'httpStatus' => Config::HTTP_STATUS_CODE_BAD_REQUEST'data' => null'error' => 'No data to update.']
  957.             ], Config::HTTP_STATUS_CODE_BAD_REQUEST);
  958.         }
  959.         $updatedInvoice $this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->updateInvoice($id$dataToUpdate);
  960.         if (!$updatedInvoice) {
  961.             return new JsonResponse([
  962.                 'response' => ['success' => false'httpStatus' => 500'data' => null'error' => 'Failed to update invoice.']
  963.             ], 500);
  964.         }
  965.         $affiliateNames $this->getAffiliateNamesMap([$updatedInvoice->getMafoAffiliateId()]);
  966.         $formattedInvoice $this->formatInvoiceForResponse([
  967.             'id' => $updatedInvoice->getId(),
  968.             'mafoPublisherCabinetManagerId' => $updatedInvoice->getMafoPublisherCabinetManagerId(),
  969.             'mafoAffiliateId' => $updatedInvoice->getMafoAffiliateId(),
  970.             'amount' => $updatedInvoice->getAmount(),
  971.             'period' => $updatedInvoice->getPeriod(),
  972.             'status' => $updatedInvoice->getStatus(),
  973.             'linkToFile' => $updatedInvoice->getLinkToFile(),
  974.             'currency' => $updatedInvoice->getCurrency(),
  975.             'approvedByEmail' => $updatedInvoice->getApprovedByEmail(),
  976.             'dateApproved' => $updatedInvoice->getDateApproved(),
  977.             'dateUpdated' => $updatedInvoice->getDateUpdated(),
  978.             'dateInserted' => $updatedInvoice->getDateInserted(),
  979.         ], $affiliateNames);
  980.         return new JsonResponse([
  981.             'response' => ['success' => true'httpStatus' => Config::HTTP_STATUS_CODE_OK'data' => $formattedInvoice'message' => 'Invoice updated successfully.''error' => null]
  982.         ], Config::HTTP_STATUS_CODE_OK);
  983.     }
  984.     /**
  985.      * Parse multipart form data for PUT/PATCH requests
  986.      */
  987.     private function parseMultipartFormData(Request $request): array
  988.     {
  989.         $result = ['fields' => [], 'files' => []];
  990.         $contentType $request->headers->get('Content-Type''');
  991.         
  992.         if (!preg_match('/boundary=(.*)$/i'$contentType$matches)) {
  993.             return $result;
  994.         }
  995.         
  996.         $parts preg_split('/-+' preg_quote($matches[1], '/') . '/'$request->getContent());
  997.         
  998.         foreach ($parts as $part) {
  999.             if (empty(trim($part)) || $part === '--') continue;
  1000.             
  1001.             $segments preg_split('/\r\n\r\n/'$part2);
  1002.             if (count($segments) < 2) continue;
  1003.             
  1004.             $headers $segments[0];
  1005.             $body rtrim($segments[1], "\r\n");
  1006.             
  1007.             if (!preg_match('/Content-Disposition:.*name="([^"]+)"(?:.*filename="([^"]+)")?/i'$headers$matches)) continue;
  1008.             
  1009.             if (isset($matches[2])) {
  1010.                 $tmpFile tempnam(sys_get_temp_dir(), 'upload_');
  1011.                 file_put_contents($tmpFile$body);
  1012.                 $mimeType preg_match('/Content-Type:\s*([^\r\n]+)/i'$headers$typeMatch) ? trim($typeMatch[1]) : 'application/octet-stream';
  1013.                 $result['files'][$matches[1]] = ['name' => $matches[2], 'type' => $mimeType'tmp_name' => $tmpFile'error' => UPLOAD_ERR_OK'size' => strlen($body)];
  1014.             } else {
  1015.                 $result['fields'][$matches[1]] = $body;
  1016.             }
  1017.         }
  1018.         
  1019.         return $result;
  1020.     }
  1021.     /**
  1022.      * Download invoice file
  1023.      * @Route("/invoice-control/{id}/download", name="download_invoice_file", methods={"GET"})
  1024.      */
  1025.     public function downloadInvoiceFileAction(Request $requestint $id)
  1026.     {
  1027.         $publisherInfo $this->getUser();
  1028.         $publisherId $publisherInfo->getId();
  1029.         $mappedAffiliateIds $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
  1030.         
  1031.         // Check if invoice exists and belongs to this publisher
  1032.         $invoice $this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->getInvoiceByIdAndPublisher($id$publisherId$mappedAffiliateIds);
  1033.         if (!$invoice) {
  1034.             return new JsonResponse([
  1035.                 'response' => [
  1036.                     'success' => false,
  1037.                     'httpStatus' => 404,
  1038.                     'data' => null,
  1039.                     'error' => 'Invoice not found.'
  1040.                 ]
  1041.             ], 404);
  1042.         }   
  1043.         if (empty($invoice['linkToFile'])) {
  1044.             return new JsonResponse([
  1045.                 'response' => [
  1046.                     'success' => false,
  1047.                     'httpStatus' => 404,
  1048.                     'data' => null,
  1049.                     'error' => 'Invoice file not found.'
  1050.                 ]
  1051.             ], 404);
  1052.         }
  1053.         // Get file from S3
  1054.         $s3Directory Config::S3_CABINET_UPLOADS_DIRECTORY;
  1055.         $fileBody $this->s3->getFileFromS3($s3Directory$invoice['linkToFile']);
  1056.         return new Response($fileBody200);
  1057.     }
  1058.     /**
  1059.      * Get single invoice by ID
  1060.      * @Route("/invoice-control/{id}", name="get_invoice", methods={"GET"})
  1061.      */
  1062.     public function getInvoiceAction(Request $requestint $id)
  1063.     {
  1064.         $publisherInfo $this->getUser();
  1065.         $publisherId $publisherInfo->getId();
  1066.         $mappedAffiliateIds $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
  1067.         // Check if invoice exists and belongs to this publisher
  1068.         $invoice $this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->getInvoiceByIdAndPublisher($id$publisherId$mappedAffiliateIds);
  1069.         if (!$invoice) {
  1070.             return new JsonResponse([
  1071.                 'response' => [
  1072.                     'success' => false,
  1073.                     'httpStatus' => 404,
  1074.                     'data' => null,
  1075.                     'error' => 'Invoice not found.'
  1076.                 ]
  1077.             ], 404);
  1078.         }
  1079.         // Get affiliate names for display
  1080.         $affiliateNames $this->getAffiliateNamesMap([$invoice['mafoAffiliateId']]);
  1081.         $formattedInvoice $this->formatInvoiceForResponse($invoice$affiliateNames);
  1082.         return new JsonResponse([
  1083.             'response' => [
  1084.                 'success' => true,
  1085.                 'httpStatus' => Config::HTTP_STATUS_CODE_OK,
  1086.                 'data' => $formattedInvoice,
  1087.                 'error' => null
  1088.             ]
  1089.         ], Config::HTTP_STATUS_CODE_OK);
  1090.     }
  1091.     // ==================== INVOICE CONTROL HELPER METHODS ====================
  1092.     /**
  1093.      * Validate uploaded invoice file
  1094.      * Handles both UploadedFile objects and array format from manual parsing
  1095.      */
  1096.     private function validateInvoiceFile($file): array
  1097.     {
  1098.         $errors = [];
  1099.         // Get file properties - handle both UploadedFile and array format
  1100.         if (is_array($file)) {
  1101.             $fileSize $file['size'] ?? 0;
  1102.             $fileName $file['name'] ?? '';
  1103.             $mimeType $file['type'] ?? '';
  1104.             $extension strtolower(pathinfo($fileNamePATHINFO_EXTENSION));
  1105.         } else {
  1106.             $fileSize $file->getSize();
  1107.             $fileName $file->getClientOriginalName();
  1108.             $mimeType $file->getMimeType();
  1109.             $extension strtolower($file->getClientOriginalExtension());
  1110.         }
  1111.         // Check file size (10MB max)
  1112.         $maxFileSize Config::PUBLISHER_CABINET_INVOICE_MAX_FILE_SIZE_BYTES;
  1113.         if ($fileSize $maxFileSize) {
  1114.             $errors[] = 'File size exceeds maximum allowed size of ' Config::PUBLISHER_CABINET_INVOICE_MAX_FILE_SIZE_MB 'MB.';
  1115.         }
  1116.         // Check file extension
  1117.         $allowedExtensions Config::PUBLISHER_CABINET_INVOICE_ALLOWED_FILE_TYPES;
  1118.         if (!in_array($extension$allowedExtensions)) {
  1119.             $errors[] = 'Unsupported file format. Allowed formats: ' implode(', '$allowedExtensions) . '.';
  1120.         }
  1121.         // Check MIME type
  1122.         $allowedMimeTypes = [
  1123.             'application/pdf',
  1124.         ];
  1125.         if (!in_array($mimeType$allowedMimeTypes)) {
  1126.             $errors[] = 'Invalid file type. Please upload a valid document or image file.';
  1127.         }
  1128.         return $errors;
  1129.     }
  1130.     /**
  1131.      * Get affiliate names mapped by ID
  1132.      */
  1133.     private function getAffiliateNamesMap(array $affiliateIds): array
  1134.     {
  1135.         if (empty($affiliateIds)) {
  1136.             return [];
  1137.         }
  1138.         $affiliates $this->doctrine->getRepository(MafoAffiliates::class)->findBy(['id' => $affiliateIds]);
  1139.         $map = [];
  1140.         foreach ($affiliates as $affiliate) {
  1141.             $map[$affiliate->getId()] = $affiliate->getName();
  1142.         }
  1143.         return $map;
  1144.     }
  1145.     /**
  1146.      * Format invoice data for API response
  1147.      */
  1148.     private function formatInvoiceForResponse(array $invoice, array $affiliateNames): array
  1149.     {
  1150.         $period $invoice['period'];
  1151.         $periodPretty $period instanceof \DateTimeInterface $period->format('M Y') : '';
  1152.         $dateApproved $invoice['dateApproved'] ?? null;
  1153.         $dateApprovedPretty $dateApproved instanceof \DateTimeInterface $dateApproved->format('Y-m-d H:i:s') : '';
  1154.         $dateInserted $invoice['dateInserted'] ?? null;
  1155.         $dateInsertedPretty $dateInserted instanceof \DateTimeInterface $dateInserted->format('Y-m-d H:i:s') : '';
  1156.         $dateUpdated $invoice['dateUpdated'] ?? null;
  1157.         $dateUpdatedPretty $dateUpdated instanceof \DateTimeInterface $dateUpdated->format('Y-m-d H:i:s') : '';
  1158.         $status $invoice['status'] ?? '';
  1159.         $statusLabel Config::PUBLISHER_CABINET_INVOICE_STATUS_LABELS[$status] ?? $status;
  1160.         $canEdit $status !== Config::PUBLISHER_CABINET_INVOICE_STATUS_APPROVED;
  1161.         return [
  1162.             'id' => $invoice['id'],
  1163.             'mafoPublisherCabinetManagerId' => $invoice['mafoPublisherCabinetManagerId'],
  1164.             'mafoAffiliateId' => $invoice['mafoAffiliateId'],
  1165.             'affiliateName' => $affiliateNames[$invoice['mafoAffiliateId']] ?? 'Unknown',
  1166.             'amount' => number_format((float)$invoice['amount'], 2),
  1167.             'amountRaw' => $invoice['amount'],
  1168.             'currency' => $invoice['currency'] ?? Config::CURRENCY_USD,
  1169.             'period' => $period instanceof \DateTimeInterface $period->format('Y-m-d') : '',
  1170.             'periodPretty' => $periodPretty,
  1171.             'status' => $status,
  1172.             'statusLabel' => $statusLabel,
  1173.             'linkToFile' => $invoice['linkToFile'] ?? '',
  1174.             'approvedByEmail' => $invoice['approvedByEmail'] ?? '',
  1175.             'dateApproved' => $dateApproved instanceof \DateTimeInterface $dateApproved->format('Y-m-d H:i:s') : '',
  1176.             'dateApprovedPretty' => $dateApprovedPretty,
  1177.             'dateInserted' => $dateInserted instanceof \DateTimeInterface $dateInserted->format('Y-m-d H:i:s') : '',
  1178.             'dateInsertedPretty' => $dateInsertedPretty,
  1179.             'dateUpdated' => $dateUpdated instanceof \DateTimeInterface $dateUpdated->format('Y-m-d H:i:s') : '',
  1180.             'dateUpdatedPretty' => $dateUpdatedPretty,
  1181.             'canEdit' => $canEdit,
  1182.         ];
  1183.     }
  1184. }