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.         $downloadCSV $request->query->get('downloadCSV');
  691.         if (empty($mappedAffiliateIds)) {
  692.             if ($downloadCSV) {
  693.                 $tableColumns = [
  694.                     ['Header' => 'ID''accessor' => 'id''show' => 1],
  695.                     ['Header' => 'Affiliate ID''accessor' => 'mafoAffiliateId''show' => 1],
  696.                     ['Header' => 'Affiliate Name''accessor' => 'affiliateName''show' => 1],
  697.                     ['Header' => 'Amount''accessor' => 'amountRaw''show' => 1],
  698.                     ['Header' => 'Currency''accessor' => 'currency''show' => 1],
  699.                     ['Header' => 'Period''accessor' => 'period''show' => 1],
  700.                     ['Header' => 'Status''accessor' => 'statusLabel''show' => 1],
  701.                     ['Header' => 'Date Inserted''accessor' => 'dateInserted''show' => 1],
  702.                     ['Header' => 'Date Updated''accessor' => 'dateUpdated''show' => 1]
  703.                 ];
  704.                 return $this->commonCalls->downloadCSV($tableColumns, [], 'Invoices');
  705.             }
  706.             
  707.             return new JsonResponse([
  708.                 'response' => [
  709.                     'success' => true,
  710.                     'httpStatus' => Config::HTTP_STATUS_CODE_OK,
  711.                     'data' => ['data' => [], 'metaData' => ['total' => 0'limit' => Config::REPORTS_PAGINATION_DEFAULT_PAGE_SIZE'page' => 1'pages' => 0]],
  712.                     'message' => 'No data available.',
  713.                     'error' => null
  714.                 ]
  715.             ], Config::HTTP_STATUS_CODE_OK);
  716.         }
  717.         $filters = [];
  718.         
  719.         $statusFilter $request->query->all('status');
  720.         if (!empty($statusFilter)) {
  721.             $statusFilterArray $statusFilter;
  722.             $mappedStatuses = [];
  723.           
  724.             $statusMapping = [
  725.                 Config::APPROVED_STATUS => [
  726.                     Config::PUBLISHER_INVOICE_CONTROL_STATUS_AMOUNT_MATCHES_MAFO,
  727.                     Config::PUBLISHER_INVOICE_CONTROL_STATUS_APPROVED_FOR_PAYMENT,
  728.                     Config::PUBLISHER_INVOICE_CONTROL_STATUS_PAID
  729.                 ],
  730.                 Config::REJECTED_STATUS => [
  731.                     Config::PUBLISHER_INVOICE_CONTROL_STATUS_AMOUNT_DOES_NOT_MATCH_MAFO
  732.                 ],
  733.             ];
  734.            
  735.             foreach ($statusFilterArray as $filterValue) {
  736.                 $normalizedFilter strtolower(trim($filterValue));
  737.                 if (isset($statusMapping[$normalizedFilter])) {
  738.                     $mappedStatuses array_merge($mappedStatuses$statusMapping[$normalizedFilter]);
  739.                 } else {
  740.                     $mappedStatuses[] = $filterValue;
  741.                 }
  742.             }
  743.             if (!empty($mappedStatuses)) {
  744.                 $filters['status'] = array_unique($mappedStatuses);
  745.             }
  746.         }
  747.         
  748.         // Mafo Publisher Id filter (multiselect): affiliateId[0]=55&affiliateId[1]=838
  749.         $affiliateIdFilter $request->query->all('affiliateId');
  750.         if (!empty($affiliateIdFilter)) {
  751.             // Only allow filtering by mapped affiliate IDs (security check)
  752.             $requestedIds is_array($affiliateIdFilter) ? $affiliateIdFilter : [$affiliateIdFilter];
  753.             $validIds array_intersect(array_map('intval'$requestedIds), array_map('intval'$mappedAffiliateIds));
  754.             if (!empty($validIds)) {
  755.                 $filters['mafoAffiliateId'] = $validIds;
  756.             }
  757.         }
  758.         // Date Range filter
  759.         $dateStart $request->query->get('startDate') ? date('Y-m-d'strtotime($request->query->get('startDate'))) : null;
  760.         $dateEnd $request->query->get('endDate') ? date('Y-m-d'strtotime($request->query->get('endDate'))) : null;
  761.         // Pagination
  762.         $limit = (int)($request->query->get('limit') ?: Config::REPORTS_PAGINATION_DEFAULT_PAGE_SIZE);
  763.         $page = (int)($request->query->get('page') ?: Config::REPORTS_PAGINATION_DEFAULT_PAGE_NUMBER);
  764.         
  765.         // Sorting - map created_at to dateInserted
  766.         $sortBy $request->query->get('sortBy') ?? 'id';
  767.         $sortType strtoupper($request->query->get('sortType') ?? Config::REPORTS_PAGINATION_DEFAULT_SORT_TYPE);
  768.         
  769.         // Map sort field names
  770.         $sortFieldMap = ['created_at' => 'dateInserted''updated_at' => 'dateUpdated'];
  771.         $sortBy $sortFieldMap[$sortBy] ?? $sortBy;
  772.         // Get invoices
  773.         $invoices $this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->getInvoicesByPublisherManagerId($publisherId$mappedAffiliateIds$dateStart$dateEnd$filters);
  774.         // Get affiliate names for display
  775.         $affiliateNames $this->getAffiliateNamesMap($mappedAffiliateIds);
  776.         // Format the data (canEdit flag is set in formatInvoiceForResponse)
  777.         $formattedInvoices array_map(function($invoice) use ($affiliateNames) {
  778.             return $this->formatInvoiceForResponse($invoice$affiliateNames);
  779.         }, $invoices);
  780.         // Sorting
  781.         if (!empty($formattedInvoices) && isset($formattedInvoices[0][$sortBy])) {
  782.             $sortFlag = ($sortType === 'ASC') ? SORT_ASC SORT_DESC;
  783.             array_multisort(array_column($formattedInvoices$sortBy), $sortFlag$formattedInvoices);
  784.         }
  785.         if ($downloadCSV) {
  786.             $tableColumns = [
  787.                 ['Header' => 'ID''accessor' => 'id''show' => 1],
  788.                 ['Header' => 'Affiliate ID''accessor' => 'mafoAffiliateId''show' => 1],
  789.                 ['Header' => 'Affiliate Name''accessor' => 'affiliateName''show' => 1],
  790.                 ['Header' => 'Amount''accessor' => 'amountRaw''show' => 1],
  791.                 ['Header' => 'Currency''accessor' => 'currency''show' => 1],
  792.                 ['Header' => 'Period''accessor' => 'period''show' => 1],
  793.                 ['Header' => 'Status''accessor' => 'statusLabel''show' => 1],
  794.                 ['Header' => 'Date Inserted''accessor' => 'dateInserted''show' => 1],
  795.                 ['Header' => 'Date Updated''accessor' => 'dateUpdated''show' => 1]
  796.             ];
  797.             return $this->commonCalls->downloadCSV($tableColumns$formattedInvoices'Invoices ' $dateStart '_' $dateEnd);
  798.         }
  799.         // Pagination
  800.         $totalRecordCount count($formattedInvoices);
  801.         $noOfPages $totalRecordCount ceil($totalRecordCount $limit) : 0;
  802.         $offset $limit * ($page 1);
  803.         return new JsonResponse([
  804.             'response' => [
  805.                 'success' => true,
  806.                 'httpStatus' => Config::HTTP_STATUS_CODE_OK,
  807.                 'data' => [
  808.                     'data' => array_slice($formattedInvoices$offset$limit),
  809.                     'metaData' => ['total' => $totalRecordCount'limit' => $limit'page' => $page'pages' => (int)$noOfPages]
  810.                 ],
  811.                 'error' => null
  812.             ]
  813.         ], Config::HTTP_STATUS_CODE_OK);
  814.     }
  815.     /**
  816.      * Upload a new invoice
  817.      * @Route("/invoice-control", name="post_invoice", methods={"POST"})
  818.      */
  819.     public function postInvoiceAction(Request $request)
  820.     {
  821.         $publisherInfo $this->getUser();
  822.         $publisherId $publisherInfo->getId();
  823.         $mappedAffiliateIds $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
  824.         $errors = [];
  825.         
  826.         // Validate required fields
  827.         $mafoAffiliateId $request->request->get('mafoPartnerId');
  828.         $amount $request->request->get('amount');
  829.         $periodInput $request->request->get('period'); // Format: YYYY-MM (e.g., 2026-01)
  830.         $file $request->files->get('invoiceFile');
  831.         // Validation
  832.         if (empty($mafoAffiliateId)) {
  833.             $errors[] = 'MAFO Partner ID is required.';
  834.         } elseif (!in_array((int)$mafoAffiliateIdarray_map('intval'$mappedAffiliateIds))) {
  835.             $errors[] = 'Invalid MAFO Partner ID. You can only upload invoices for your mapped partners.';
  836.         }
  837.         if (empty($amount) || !is_numeric($amount) || $amount <= 0) {
  838.             $errors[] = 'Amount is required and must be a positive number.';
  839.         }
  840.         if (empty($periodInput)) {
  841.             $errors[] = 'Period (Year-Month) is required.';
  842.         } elseif (!preg_match('/^\d{4}-\d{2}$/'$periodInput)) {
  843.             $errors[] = 'Period must be in YYYY-MM format.';
  844.         }
  845.         if (!$file) {
  846.             $errors[] = 'Invoice file is required.';
  847.         } else {
  848.             // Validate file
  849.             $fileValidation $this->validateInvoiceFile($file);
  850.             if (!empty($fileValidation)) {
  851.                 $errors array_merge($errors$fileValidation);
  852.             }
  853.         }
  854.         if (!empty($errors)) {
  855.             return new JsonResponse([
  856.                 'response' => [
  857.                     'success' => false,
  858.                     'httpStatus' => Config::HTTP_STATUS_CODE_BAD_REQUEST,
  859.                     'data' => null,
  860.                     'error' => implode(' '$errors)
  861.                 ]
  862.             ], Config::HTTP_STATUS_CODE_BAD_REQUEST);
  863.         }
  864.         $period = new \DateTime($periodInput '-01');
  865.         $invoiceRepository $this->doctrine->getRepository(MafoPublisherCabinetInvoice::class);
  866.         if ($invoiceRepository->invoiceExistsForPeriod($publisherId, (int)$mafoAffiliateId$period)) {
  867.             return new JsonResponse([
  868.                 'response' => [
  869.                     'success' => false,
  870.                     'httpStatus' => Config::HTTP_STATUS_CODE_BAD_REQUEST,
  871.                     'data' => null,
  872.                     'error' => 'An invoice already exists for this MAFO Partner ID and period (' $periodInput '). Only one invoice is allowed per month per partner.'
  873.                 ]
  874.             ], Config::HTTP_STATUS_CODE_BAD_REQUEST);
  875.         }
  876.         // Upload file to S3 (cabinet-uploads directory for GuardDuty scanning)
  877.         $s3Directory Config::S3_CABINET_UPLOADS_DIRECTORY;
  878.         $uploadDate = (new \DateTime())->format('Y-m-d');
  879.         $uploadTimestamp time();
  880.         $originalFileName $file->getClientOriginalName();
  881.         $linkToFile $s3Directory '/publisher/invoice-' $publisherId '-' $mafoAffiliateId '-' $uploadDate '-' $originalFileName '-' $uploadTimestamp;
  882.         
  883.         $uploadedFileKey $this->s3->uploadFileToS3($file->getPathname(), $linkToFile);
  884.         if (!$uploadedFileKey) {
  885.             return new JsonResponse([
  886.                 'response' => [
  887.                     'success' => false,
  888.                     'httpStatus' => 500,
  889.                     'data' => null,
  890.                     'error' => 'Failed to upload file. Please try again.'
  891.                 ]
  892.             ], 500);
  893.         }
  894.         try {
  895.             $invoice $this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->createInvoice(
  896.                 $publisherId,
  897.                 (int)$mafoAffiliateId,
  898.                 $amount,
  899.                 $period,
  900.                 $uploadedFileKey,
  901.                 $request->request->get('currency') ?? Config::CURRENCY_USD,
  902.                 Config::PUBLISHER_INVOICE_CONTROL_STATUS_AMOUNT_DOES_NOT_MATCH_MAFO
  903.             );
  904.             $periodString $period->format('Y-m-1');
  905.             $affiliateId = (int)$mafoAffiliateId;
  906.             
  907.             $this->mafoFinancialToolsComponents->syncPublisherInvoiceStatusFromPayoutTotal(
  908.                 $periodString,
  909.                 $affiliateId
  910.             );
  911.             $this->doctrine->getManager()->refresh($invoice);
  912.             // Get affiliate name for response
  913.             $affiliateNames $this->getAffiliateNamesMap([(int)$mafoAffiliateId]);
  914.             $formattedInvoice $this->formatInvoiceForResponse([
  915.                 'id' => $invoice->getId(),
  916.                 'mafoPublisherCabinetManagerId' => $invoice->getMafoPublisherCabinetManagerId(),
  917.                 'mafoAffiliateId' => $invoice->getMafoAffiliateId(),
  918.                 'amount' => $invoice->getAmount(),
  919.                 'period' => $invoice->getPeriod(),
  920.                 'status' => $invoice->getStatus(),
  921.                 'linkToFile' => $invoice->getLinkToFile(),
  922.                 'currency' => $invoice->getCurrency(),
  923.                 'approvedByEmail' => $invoice->getApprovedByEmail(),
  924.                 'dateApproved' => $invoice->getDateApproved(),
  925.                 'dateUpdated' => $invoice->getDateUpdated(),
  926.                 'dateInserted' => $invoice->getDateInserted(),
  927.             ], $affiliateNames);
  928.             return new JsonResponse([
  929.                 'response' => [
  930.                     'success' => true,
  931.                     'httpStatus' => 201,
  932.                     'data' => $formattedInvoice,
  933.                     'message' => 'Invoice uploaded successfully.',
  934.                     'error' => null
  935.                 ]
  936.             ], 201);
  937.         } catch (\Exception $e) {
  938.             // Delete the uploaded file if invoice creation failed
  939.             $this->s3->deleteFileFromS3($uploadedFileKey);
  940.             return new JsonResponse([
  941.                 'response' => [
  942.                     'success' => false,
  943.                     'httpStatus' => 500,
  944.                     'data' => null,
  945.                     'error' => 'Failed to create invoice record. Please try again.'
  946.                 ]
  947.             ], 500);
  948.         }
  949.     }
  950.     /**
  951.      * Update an existing invoice (only amount and file editable, not for APPROVED status)
  952.      * @Route("/invoice-control/{id}", name="put_invoice", methods={"PUT"})
  953.      */
  954.     public function updateInvoiceAction(Request $requestint $id)
  955.     {
  956.         $publisherId $this->getUser()->getId();
  957.         $mappedAffiliateIds $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
  958.         $invoice $this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->getInvoiceByIdAndPublisher($id$publisherId$mappedAffiliateIds);
  959.         if (!$invoice) {
  960.             return new JsonResponse([
  961.                 'response' => ['success' => false'httpStatus' => 404'data' => null'error' => 'Invoice not found.']
  962.             ], 404);
  963.         }
  964.         if (!$this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->canEditInvoice($id)) {
  965.             return new JsonResponse([
  966.                 'response' => ['success' => false'httpStatus' => Config::HTTP_STATUS_CODE_FORBIDDEN'data' => null'error' => 'Cannot edit an approved invoice.']
  967.             ], Config::HTTP_STATUS_CODE_FORBIDDEN);
  968.         }
  969.         // Parse request data based on content type
  970.         $contentType $request->headers->get('Content-Type''');
  971.         $method $request->getMethod();
  972.         
  973.         if (strpos($contentType'application/json') !== false) {
  974.             $requestData json_decode($request->getContent(), true) ?? [];
  975.             $amount $requestData['amount'] ?? null;
  976.             $file null;
  977.         } elseif (strpos($contentType'multipart/form-data') !== false && in_array($method, ['PUT''PATCH'])) {
  978.             $parsedData $this->parseMultipartFormData($request);
  979.             $amount $parsedData['fields']['amount'] ?? null;
  980.             $file $parsedData['files']['invoiceFile'] ?? null;
  981.         } else {
  982.             $amount $request->request->get('amount');
  983.             $file $request->files->get('invoiceFile');
  984.         }
  985.         $errors = [];
  986.         $dataToUpdate = [];
  987.         // Validate amount
  988.         if ($amount !== null && $amount !== '') {
  989.             if (!is_numeric($amount) || $amount <= 0) {
  990.                 $errors[] = 'Amount must be a positive number.';
  991.             } else {
  992.                 $dataToUpdate['amount'] = $amount;
  993.             }
  994.         }
  995.         // Handle file update
  996.         if ($file) {
  997.             $fileValidation $this->validateInvoiceFile($file);
  998.             if (!empty($fileValidation)) {
  999.                 $errors array_merge($errors$fileValidation);
  1000.             } else {
  1001.                 if (!empty($invoice['linkToFile'])) {
  1002.                     $this->s3->deleteFileFromS3($invoice['linkToFile']);
  1003.                 }
  1004.                 $filePath is_array($file) ? $file['tmp_name'] : $file->getPathname();
  1005.                 $fileName is_array($file) ? $file['name'] : $file->getClientOriginalName();
  1006.                 $fileKey Config::S3_CABINET_UPLOADS_DIRECTORY '/' $publisherId '/' time() . '-' $fileName;
  1007.                 $uploadedFileKey $this->s3->uploadFileToS3($filePath$fileKey);
  1008.                 
  1009.                 if ($uploadedFileKey) {
  1010.                     $dataToUpdate['linkToFile'] = $uploadedFileKey;
  1011.                 } else {
  1012.                     $errors[] = 'Failed to upload file.';
  1013.                 }
  1014.             }
  1015.         }
  1016.         if (!empty($errors)) {
  1017.             return new JsonResponse([
  1018.                 'response' => ['success' => false'httpStatus' => Config::HTTP_STATUS_CODE_BAD_REQUEST'data' => null'error' => implode(' '$errors)]
  1019.             ], Config::HTTP_STATUS_CODE_BAD_REQUEST);
  1020.         }
  1021.         if (empty($dataToUpdate)) {
  1022.             return new JsonResponse([
  1023.                 'response' => ['success' => false'httpStatus' => Config::HTTP_STATUS_CODE_BAD_REQUEST'data' => null'error' => 'No data to update.']
  1024.             ], Config::HTTP_STATUS_CODE_BAD_REQUEST);
  1025.         }
  1026.         $updatedInvoice $this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->updateInvoice($id$dataToUpdate);
  1027.         if (!$updatedInvoice) {
  1028.             return new JsonResponse([
  1029.                 'response' => ['success' => false'httpStatus' => 500'data' => null'error' => 'Failed to update invoice.']
  1030.             ], 500);
  1031.         }
  1032.         $periodString $updatedInvoice->getPeriod()->format('Y-m-1');
  1033.         $affiliateId $updatedInvoice->getMafoAffiliateId();
  1034.         
  1035.         $this->mafoFinancialToolsComponents->syncPublisherInvoiceStatusFromPayoutTotal(
  1036.             $periodString,
  1037.             $affiliateId
  1038.         );
  1039.         $this->doctrine->getManager()->refresh($updatedInvoice);
  1040.         $affiliateNames $this->getAffiliateNamesMap([$updatedInvoice->getMafoAffiliateId()]);
  1041.         $formattedInvoice $this->formatInvoiceForResponse([
  1042.             'id' => $updatedInvoice->getId(),
  1043.             'mafoPublisherCabinetManagerId' => $updatedInvoice->getMafoPublisherCabinetManagerId(),
  1044.             'mafoAffiliateId' => $updatedInvoice->getMafoAffiliateId(),
  1045.             'amount' => $updatedInvoice->getAmount(),
  1046.             'period' => $updatedInvoice->getPeriod(),
  1047.             'status' => $updatedInvoice->getStatus(),
  1048.             'linkToFile' => $updatedInvoice->getLinkToFile(),
  1049.             'currency' => $updatedInvoice->getCurrency(),
  1050.             'approvedByEmail' => $updatedInvoice->getApprovedByEmail(),
  1051.             'dateApproved' => $updatedInvoice->getDateApproved(),
  1052.             'dateUpdated' => $updatedInvoice->getDateUpdated(),
  1053.             'dateInserted' => $updatedInvoice->getDateInserted(),
  1054.         ], $affiliateNames);
  1055.         return new JsonResponse([
  1056.             'response' => ['success' => true'httpStatus' => Config::HTTP_STATUS_CODE_OK'data' => $formattedInvoice'message' => 'Invoice updated successfully.''error' => null]
  1057.         ], Config::HTTP_STATUS_CODE_OK);
  1058.     }
  1059.     /**
  1060.      * Parse multipart form data for PUT/PATCH requests
  1061.      */
  1062.     private function parseMultipartFormData(Request $request): array
  1063.     {
  1064.         $result = ['fields' => [], 'files' => []];
  1065.         $contentType $request->headers->get('Content-Type''');
  1066.         
  1067.         if (!preg_match('/boundary=(.*)$/i'$contentType$matches)) {
  1068.             return $result;
  1069.         }
  1070.         
  1071.         $parts preg_split('/-+' preg_quote($matches[1], '/') . '/'$request->getContent());
  1072.         
  1073.         foreach ($parts as $part) {
  1074.             if (empty(trim($part)) || $part === '--') continue;
  1075.             
  1076.             $segments preg_split('/\r\n\r\n/'$part2);
  1077.             if (count($segments) < 2) continue;
  1078.             
  1079.             $headers $segments[0];
  1080.             $body rtrim($segments[1], "\r\n");
  1081.             
  1082.             if (!preg_match('/Content-Disposition:.*name="([^"]+)"(?:.*filename="([^"]+)")?/i'$headers$matches)) continue;
  1083.             
  1084.             if (isset($matches[2])) {
  1085.                 $tmpFile tempnam(sys_get_temp_dir(), 'upload_');
  1086.                 file_put_contents($tmpFile$body);
  1087.                 $mimeType preg_match('/Content-Type:\s*([^\r\n]+)/i'$headers$typeMatch) ? trim($typeMatch[1]) : 'application/octet-stream';
  1088.                 $result['files'][$matches[1]] = ['name' => $matches[2], 'type' => $mimeType'tmp_name' => $tmpFile'error' => UPLOAD_ERR_OK'size' => strlen($body)];
  1089.             } else {
  1090.                 $result['fields'][$matches[1]] = $body;
  1091.             }
  1092.         }
  1093.         
  1094.         return $result;
  1095.     }
  1096.     /**
  1097.      * Download invoice file
  1098.      * @Route("/invoice-control/{id}/download", name="download_invoice_file", methods={"GET"})
  1099.      */
  1100.     public function downloadInvoiceFileAction(Request $requestint $id)
  1101.     {
  1102.         $publisherInfo $this->getUser();
  1103.         $publisherId $publisherInfo->getId();
  1104.         $mappedAffiliateIds $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
  1105.         
  1106.         // Check if invoice exists and belongs to this publisher
  1107.         $invoice $this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->getInvoiceByIdAndPublisher($id$publisherId$mappedAffiliateIds);
  1108.         if (!$invoice) {
  1109.             return new JsonResponse([
  1110.                 'response' => [
  1111.                     'success' => false,
  1112.                     'httpStatus' => 404,
  1113.                     'data' => null,
  1114.                     'error' => 'Invoice not found.'
  1115.                 ]
  1116.             ], 404);
  1117.         }   
  1118.         if (empty($invoice['linkToFile'])) {
  1119.             return new JsonResponse([
  1120.                 'response' => [
  1121.                     'success' => false,
  1122.                     'httpStatus' => 404,
  1123.                     'data' => null,
  1124.                     'error' => 'Invoice file not found.'
  1125.                 ]
  1126.             ], 404);
  1127.         }
  1128.         // Get file from S3
  1129.         $s3Directory Config::S3_CABINET_UPLOADS_DIRECTORY;
  1130.         $fileBody $this->s3->getFileFromS3($s3Directory$invoice['linkToFile']);
  1131.         return new Response($fileBody200);
  1132.     }
  1133.     /**
  1134.      * Get single invoice by ID
  1135.      * @Route("/invoice-control/{id}", name="get_invoice", methods={"GET"})
  1136.      */
  1137.     public function getInvoiceAction(Request $requestint $id)
  1138.     {
  1139.         $publisherInfo $this->getUser();
  1140.         $publisherId $publisherInfo->getId();
  1141.         $mappedAffiliateIds $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
  1142.         // Check if invoice exists and belongs to this publisher
  1143.         $invoice $this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->getInvoiceByIdAndPublisher($id$publisherId$mappedAffiliateIds);
  1144.         if (!$invoice) {
  1145.             return new JsonResponse([
  1146.                 'response' => [
  1147.                     'success' => false,
  1148.                     'httpStatus' => 404,
  1149.                     'data' => null,
  1150.                     'error' => 'Invoice not found.'
  1151.                 ]
  1152.             ], 404);
  1153.         }
  1154.         // Get affiliate names for display
  1155.         $affiliateNames $this->getAffiliateNamesMap([$invoice['mafoAffiliateId']]);
  1156.         $formattedInvoice $this->formatInvoiceForResponse($invoice$affiliateNames);
  1157.         return new JsonResponse([
  1158.             'response' => [
  1159.                 'success' => true,
  1160.                 'httpStatus' => Config::HTTP_STATUS_CODE_OK,
  1161.                 'data' => $formattedInvoice,
  1162.                 'error' => null
  1163.             ]
  1164.         ], Config::HTTP_STATUS_CODE_OK);
  1165.     }
  1166.     // ==================== INVOICE CONTROL HELPER METHODS ====================
  1167.     /**
  1168.      * Validate uploaded invoice file
  1169.      * Handles both UploadedFile objects and array format from manual parsing
  1170.      */
  1171.     private function validateInvoiceFile($file): array
  1172.     {
  1173.         $errors = [];
  1174.         // Get file properties - handle both UploadedFile and array format
  1175.         if (is_array($file)) {
  1176.             $fileSize $file['size'] ?? 0;
  1177.             $fileName $file['name'] ?? '';
  1178.             $mimeType $file['type'] ?? '';
  1179.             $extension strtolower(pathinfo($fileNamePATHINFO_EXTENSION));
  1180.         } else {
  1181.             $fileSize $file->getSize();
  1182.             $fileName $file->getClientOriginalName();
  1183.             $mimeType $file->getMimeType();
  1184.             $extension strtolower($file->getClientOriginalExtension());
  1185.         }
  1186.         // Check file size (10MB max)
  1187.         $maxFileSize Config::PUBLISHER_CABINET_INVOICE_MAX_FILE_SIZE_BYTES;
  1188.         if ($fileSize $maxFileSize) {
  1189.             $errors[] = 'File size exceeds maximum allowed size of ' Config::PUBLISHER_CABINET_INVOICE_MAX_FILE_SIZE_MB 'MB.';
  1190.         }
  1191.         // Check file extension
  1192.         $allowedExtensions Config::PUBLISHER_CABINET_INVOICE_ALLOWED_FILE_TYPES;
  1193.         if (!in_array($extension$allowedExtensions)) {
  1194.             $errors[] = 'Unsupported file format. Allowed formats: ' implode(', '$allowedExtensions) . '.';
  1195.         }
  1196.         // Check MIME type
  1197.         $allowedMimeTypes = [
  1198.             'application/pdf',
  1199.         ];
  1200.         if (!in_array($mimeType$allowedMimeTypes)) {
  1201.             $errors[] = 'Invalid file type. Please upload a valid document or image file.';
  1202.         }
  1203.         return $errors;
  1204.     }
  1205.     /**
  1206.      * Get affiliate names mapped by ID
  1207.      */
  1208.     private function getAffiliateNamesMap(array $affiliateIds): array
  1209.     {
  1210.         if (empty($affiliateIds)) {
  1211.             return [];
  1212.         }
  1213.         $affiliates $this->doctrine->getRepository(MafoAffiliates::class)->findBy(['id' => $affiliateIds]);
  1214.         $map = [];
  1215.         foreach ($affiliates as $affiliate) {
  1216.             $map[$affiliate->getId()] = $affiliate->getName();
  1217.         }
  1218.         return $map;
  1219.     }
  1220.     /**
  1221.      * Format invoice data for API response
  1222.      */
  1223.     private function formatInvoiceForResponse(array $invoice, array $affiliateNames): array
  1224.     {
  1225.         $period $invoice['period'];
  1226.         $periodPretty $period instanceof \DateTimeInterface $period->format('M Y') : '';
  1227.         $dateInserted $invoice['dateInserted'] ?? null;
  1228.         $dateInsertedPretty $dateInserted instanceof \DateTimeInterface $dateInserted->format('Y-m-d H:i:s') : '';
  1229.         $dateUpdated $invoice['dateUpdated'] ?? null;
  1230.         $dateUpdatedPretty $dateUpdated instanceof \DateTimeInterface $dateUpdated->format('Y-m-d H:i:s') : '';
  1231.         $status $invoice['status'] ?? '';
  1232.         
  1233.         $approvedStatuses = [
  1234.             Config::PUBLISHER_CABINET_INVOICE_STATUS_APPROVED,
  1235.             Config::PUBLISHER_INVOICE_CONTROL_STATUS_AMOUNT_MATCHES_MAFO,
  1236.             Config::PUBLISHER_INVOICE_CONTROL_STATUS_APPROVED_FOR_PAYMENT,
  1237.             Config::PUBLISHER_INVOICE_CONTROL_STATUS_PAID
  1238.         ];
  1239.         $rejectedStatuses = [
  1240.             Config::PUBLISHER_CABINET_INVOICE_STATUS_REJECTED,
  1241.             Config::PUBLISHER_INVOICE_CONTROL_STATUS_AMOUNT_DOES_NOT_MATCH_MAFO
  1242.         ]; 
  1243.         
  1244.         $displayStatus $status;
  1245.         $displayStatusLabel $status;
  1246.         
  1247.         if (in_array($status$approvedStatusestrue)) {
  1248.             $displayStatus Config::APPROVED_STATUS;
  1249.             $displayStatusLabel Config::PUBLISHER_CABINET_INVOICE_STATUS_LABELS[Config::PUBLISHER_CABINET_INVOICE_STATUS_APPROVED];
  1250.         } elseif (in_array($status$rejectedStatusestrue)) {
  1251.             $displayStatus Config::REJECTED_STATUS;
  1252.             $displayStatusLabel Config::PUBLISHER_CABINET_INVOICE_STATUS_LABELS[Config::PUBLISHER_CABINET_INVOICE_STATUS_REJECTED];
  1253.         }
  1254.         
  1255.         // Keep original status for canEdit check (check against actual database status)
  1256.         $canEdit = !in_array($status$approvedStatusestrue);
  1257.         // Return only allowed fields (filtered for publisher managers)
  1258.         return [
  1259.             'id' => $invoice['id'],
  1260.             'mafoAffiliateId' => $invoice['mafoAffiliateId'],
  1261.             'affiliateName' => $affiliateNames[$invoice['mafoAffiliateId']] ?? 'Unknown',
  1262.             'amount' => number_format((float)$invoice['amount'], 2),
  1263.             'amountRaw' => $invoice['amount'],
  1264.             'currency' => $invoice['currency'] ?? Config::CURRENCY_USD,
  1265.             'period' => $period instanceof \DateTimeInterface $period->format('Y-m-d') : '',
  1266.             'periodPretty' => $periodPretty,
  1267.             'status' => $displayStatus// Display status (approved/rejected)
  1268.             'statusLabel' => $displayStatusLabel// Display label (Approved/Rejected)
  1269.             'linkToFile' => $invoice['linkToFile'] ?? '',
  1270.             'dateInserted' => $dateInsertedPretty,
  1271.             'dateUpdated' => $dateUpdatedPretty,
  1272.             'canEdit' => $canEdit,
  1273.         ];
  1274.     }
  1275. }