<?php
namespace App\Controller;
use App\Config;
use App\Entity\MafoId\MafoAffiliates;
use App\Entity\MafoId\MafoOffers;
use App\Entity\Tune\AffiliateInfo;
use App\Entity\Employees;
use App\Entity\AgentControl;
use App\Entity\MmpMobileApps;
use App\Entity\MmpOffers;
use App\Services\AffiliateHasofferAPI;
use App\Services\Alerts;
use App\Services\Aws\ElasticCache;
use App\Services\Aws\S3;
use App\Services\BrandHasofferAPI;
use App\Services\Common;
use App\Services\MafoFinancialToolsComponents;
use App\Services\ImpressionsApis;
use App\Services\Metrics24APICalls;
use App\Services\MmpComponents;
use App\Services\MysqlQueries;
use App\Services\UsersComponents;
use Doctrine\Persistence\ManagerRegistry;
use Mmoreram\GearmanBundle\Service\GearmanClientInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use App\Repository\MafoPublisherCabinetManagerMappingWithAffiliateRepository;
use App\Entity\MafoPublisherCabinetInvoice;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use function GuzzleHttp\json_encode;
use App\Traits\TrafficReportTrait;
/**
*
* Offer related routes with endpoint /api/offers/{route}
*
* @Route("/api/client/publisher", name="client_publisher_", host="%publishers_subdomain%")
*/
class ClientSidePublisherController extends AbstractController
{
use TrafficReportTrait;
private $commonCalls;
private $doctrine;
private $mysqlQueries;
private $mafoFinancialToolsComponents;
private $alerts;
private $brandHasofferApi;
private $mmpComponents;
private $affiliateHasofferAPI;
private $usersComponents;
private $elasticCache;
private $projectDir;
private $s3;
private $impressionsApis;
private $metrics24APICalls;
private $gearmanClientInterface;
private $mappingRepository;
public function __construct(
Common $commonCalls,
ManagerRegistry $doctrine,
MysqlQueries $mysqlQueries,
MafoPublisherCabinetManagerMappingWithAffiliateRepository $mappingRepository,
MafoFinancialToolsComponents $mafoFinancialToolsComponents,
Alerts $alerts,
BrandHasofferApi $brandHasofferApi,
MmpComponents $mmpComponents,
AffiliateHasofferAPI $affiliateHasofferAPI,
UsersComponents $usersComponents,
ElasticCache $elasticCache,
S3 $s3,
ImpressionsApis $impressionsApis,
Metrics24APICalls $metrics24APICalls,
GearmanClientInterface $gearmanClientInterface,
string $projectDir
) {
$this->commonCalls = $commonCalls;
$this->doctrine = $doctrine;
$this->mysqlQueries = $mysqlQueries;
$this->mafoFinancialToolsComponents = $mafoFinancialToolsComponents;
$this->alerts = $alerts;
$this->brandHasofferApi = $brandHasofferApi;
$this->mmpComponents = $mmpComponents;
$this->affiliateHasofferAPI = $affiliateHasofferAPI;
$this->usersComponents = $usersComponents;
$this->elasticCache = $elasticCache;
$this->s3 = $s3;
$this->impressionsApis = $impressionsApis;
$this->metrics24APICalls = $metrics24APICalls;
$this->gearmanClientInterface = $gearmanClientInterface;
$this->projectDir = $projectDir;
$this->mappingRepository = $mappingRepository;
}
/**
* @Route("/validate", name="validate", methods={"GET"})
*/
public function getValidateAction(Request $request)
{
return new JsonResponse(true);
}
/**
* @Route("/test", name="test", methods={"GET"})
*/
public function getAgentControlAction(Request $request)
{
return new JsonResponse(true);
}
/**
* @Route("/login", name="login")
*/
public function indexAction(AuthenticationUtils $authenticationUtils)
{
// $authenticationUtils = $authenticationUtils->get('security.authentication_utils');
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('/publisher/login/index.html.twig', [
'last_username' => $lastUsername,
'error' => $error,
]);
}
/**
* @Route("/details", name="get_publisher_details", methods={"GET"})
*/
public function getPublishersDetails(Request $request): Response
{
$publisherInfo = $this->getUser();
$publisherId = $publisherInfo->getId();
$mappedAffiliateIds = $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
$publisherData = [
'id' => $publisherInfo->getId(),
'email' => $publisherInfo->getEmail(),
'firstName' => $publisherInfo->getFirstName(),
'lastName' => $publisherInfo->getLastName(),
'status' => $publisherInfo->getStatus(),
'lastLoginAt' => $publisherInfo->getLastLoginAt(),
'dateUpdated' => $publisherInfo->getDateUpdated(),
];
return $this->json($publisherData);
}
/**
* @Route("/report-columns/{report}", name="get_report_columns", methods={"GET"})
*/
public function getReportColumnsAction(Request $request, $report)
{
if (in_array($report, array_keys(Config::TABLE_COLUMNS_WITH_JSON_FILE))) {
return new JsonResponse(array_values($this->commonCalls->getDataFromJsonFile(Config::TABLE_COLUMNS_WITH_JSON_FILE[$report])));
}
}
/**
* @Route("/hyper-statuses", name="get_hyper_statuses", methods={"GET"})
*/
public function getHyperStatusesAction(Request $request)
{
$arr = [];
foreach (array_values(Config::HYPER_STATUS_MAFO_MACROS) as $key => $value) {
$arr[$value] = [
'value' => $value,
'label' => $value,
];
}
return new JsonResponse(array_values($arr));
}
/**
* @Route("/affiliates", name="get_affiliates", methods={"GET"})
*/
public function getAffiliatesAction(Request $request)
{
$publisherInfo = $this->getUser();
$publisherId = $publisherInfo->getId();
$mappedAffiliateIds = $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
$keyword = $request->query->get('keyword');
$dataByKeyword = [];
if ($keyword) {
$dataByKeyword = $this->doctrine->getRepository(AffiliateInfo::class)->getAffiliateByKeyword($keyword);
}
$statusArr = $request->query->get('status') != '' ? $request->query->get('status') : [Config::ACTIVE_STATUS];
$affiliateData = $this->commonCalls->getPublisherAffiliateListByStatusWithKeys($statusArr, $mappedAffiliateIds);
$affiliateList = [];
foreach ($affiliateData as $key => $value) {
$affiliateList[$value['id']] = [
'value' => $value['id'],
'label' => $value['id'] . ' - ' . $value['name'],
'status' => $value['status']
];
}
foreach ($dataByKeyword as $key => $value) {
if (!array_key_exists($value['affiliateId'], $affiliateList)) {
$temp = [
'value' => (int)$value['affiliateId'],
'label' => $value['affiliateId'] . ' - ' . $value['company'],
'status' => $value['status']
];
$affiliateList[$value['affiliateId']] = $temp;
}
}
ksort($affiliateList);
return new JsonResponse(array_values($affiliateList));
}
/**
* @Route("/mafo-users", name="get_mafo_users", methods={"GET"})
*/
public function getAffiliateManagersAction(Request $request)
{
$affiliateData = $this->getAffiliateAndManagerData();
$mappedAffiliateIds = $affiliateData['mappedAffiliateIds'];
if (empty($mappedAffiliateIds)) {
return new JsonResponse([]);
}
$accountManagerEmails = $this->doctrine->getRepository(MafoAffiliates::class)
->getAccountManagerEmailsByAffiliateIds($mappedAffiliateIds);
if (empty($accountManagerEmails)) {
return new JsonResponse([]);
}
$employeesInfo = $this->doctrine->getRepository(Employees::class)
->getEmployeesByEmails($accountManagerEmails);
$responseArr = [];
foreach ($employeesInfo as $email => $employee) {
$responseArr[] = [
'value' => $employee['email'] ?? '',
'label' => $employee['fullName'] . "[{$employee['email']}]"
];
}
usort($responseArr, fn($a, $b) => strcmp($a['label'], $b['label']));
return new JsonResponse($responseArr);
}
private function getAffiliateAndManagerData()
{
$publisherInfo = $this->getUser();
$publisherId = $publisherInfo->getId();
$mappedAffiliateIds = $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
$accountManagerEmails = $this->doctrine->getRepository(MafoAffiliates::class)
->getAccountManagerEmailsByAffiliateIds($mappedAffiliateIds);
$mappedAffiliateIds = array_map('strval', $mappedAffiliateIds);
return [
'formattedArray' => [
'MULTISELECT_MAFO_AFFILIATES' => $mappedAffiliateIds,
],
'formattedTrafficReportArray' => [
'MULTISELECT_MAFO_AFFILIATES' => $mappedAffiliateIds,
],
'mappedAffiliateIds' => $mappedAffiliateIds,
'accountManagerEmails' => $accountManagerEmails,
];
}
/**
* @Route("/financial-report", name="get_financial_report", methods={"GET"})
*/
public function getfinancialReportAction(Request $request)
{
$affiliateData = $this->getAffiliateAndManagerData();
$formattedArray = $affiliateData['formattedArray'];
$mappedAffiliateIds = $affiliateData['mappedAffiliateIds'];
$mappedAccountManagerEmails = $affiliateData['accountManagerEmails'];
if (empty($mappedAffiliateIds)) {
return new JsonResponse([
'response' => [
'success' => true,
'httpStatus' => Config::HTTP_STATUS_CODE_OK,
'data' => [],
'message' => 'No data available.',
'error' => null
]
], Config::HTTP_STATUS_CODE_OK);
}
$selectedColumns = $request->query->all('data') != '' ? $request->query->all('data') : [];
$validationError = $this->validatePublisherFinancialReportFields($selectedColumns);
if ($validationError !== null) {
return new JsonResponse($validationError, Config::HTTP_STATUS_CODE_BAD_REQUEST);
}
$filtersSelected = $request->query->all('filters') ?? [];
$excludedFlagForFilters = $request->query->all('excludedFlagForFilters') ?? [];
$processedFilters = $this->processPublisherFinancialReportFilters(
$filtersSelected,
$excludedFlagForFilters,
$mappedAffiliateIds
);
$filters = $processedFilters['filters'];
$excludedFiltersFlags = $processedFilters['excludedFiltersFlags'];
$limit = $request->query->get('limit') ? $request->query->get('limit') : Config::REPORTS_PAGINATION_DEFAULT_PAGE_SIZE;
$page = $request->query->get('page') ? $request->query->get('page') : Config::REPORTS_PAGINATION_DEFAULT_PAGE_NUMBER;
$sortBy = $request->query->get('sortBy') ?? Config::REPORTS_PAGINATION_DEFAULT_SORT_BY;
$sortType = $request->query->get('sortType') ?? Config::REPORTS_PAGINATION_DEFAULT_SORT_TYPE;
$dateStart = date('Y-m-1', strtotime('-0 month', strtotime($request->query->get('startDate'))));
$dateEnd = date('Y-m-1', strtotime('+0 month ', strtotime($request->query->get('endDate'))));
if ($request->query->get('downloadCSV') == 'true') {
$limit = Config::REPORTS_PAGINATION_DEFAULT_CSV_PAGE_SIZE;
$page = Config::REPORTS_PAGINATION_DEFAULT_PAGE_NUMBER;
}
$finalArr = $this->mafoFinancialToolsComponents->getPayoutTotalAggregatedData($dateStart, $dateEnd, $filters, $excludedFiltersFlags, $selectedColumns, null);
$allowedStatuses = array_values(Config::HYPER_STATUS_AFFILIATE_MAFO_MACROS);
foreach ($finalArr as &$record) {
if (!in_array($record['hyperStatus'], $allowedStatuses)) {
$record['hyperStatus'] = '';
}
}
$tableColumns = $this->commonCalls->changeColumnVisibilityForTable(array_values($this->commonCalls->getDataFromJsonFile(Config::JSON_FILE_CLIENT_SIDE_PUBLIHSER_FINANCIAL_REPORT)), $selectedColumns, []);
if ($request->query->get('downloadCSV') == 'true') {
$this->commonCalls->downloadCSV($tableColumns, $finalArr, 'Financial Report ' . $dateStart . '_' . $dateEnd);
}
return new JsonResponse($this->commonCalls->getReportResponse($finalArr, $tableColumns, $limit, $page, $sortBy, $sortType));
}
/**
* @Route("/traffic-report", methods={"GET"})
*/
public function getTrafficReport(Request $request)
{
$affiliateData = $this->getAffiliateAndManagerData();
$formattedTrafficReportArray = $affiliateData['formattedTrafficReportArray'];
$mappedAffiliateIds = $affiliateData['mappedAffiliateIds'];
if (empty($mappedAffiliateIds)) {
return new JsonResponse([
'response' => [
'success' => true,
'httpStatus' => Config::HTTP_STATUS_CODE_OK,
'data' => [],
'message' => 'No data available.',
'error' => null
]
], Config::HTTP_STATUS_CODE_OK);
}
$selectedColumns = $request->query->all('data') != '' ? $request->query->all('data') : [];
$validationError = $this->validatePublisherTrafficReportFields($selectedColumns);
if ($validationError !== null) {
return new JsonResponse($validationError, Config::HTTP_STATUS_CODE_BAD_REQUEST);
}
$filtersSelected = $request->query->all('filters') ?? [];
$excludedFlagForFilters = $request->query->all('excludedFlagForFilters') ?? [];
$processedFilters = $this->processPublisherTrafficReportFilters(
$filtersSelected,
$excludedFlagForFilters,
$mappedAffiliateIds
);
$filters = $processedFilters['filters'];
$excludedFiltersFlags = $processedFilters['excludedFiltersFlags'];
$selectedColumns = $request->query->all('data') != '' ? $request->query->all('data') : [];
$groupedColumns = $request->query->all('groups') != '' ? $request->query->all('groups') : [];
$dateStart = date('Y-m-d', strtotime($request->query->get('startDate')));
$dateEnd = date('Y-m-d', strtotime($request->query->get('endDate')));
$eventTimestampFrom = strtotime($dateStart);
$eventTimeStampTo = strtotime($dateEnd);
$sortBy = $request->query->get('sortBy') ?? Config::REPORTS_PAGINATION_MMP_REPORT_DEFAULT_SORT_BY;
$sortType = $request->query->get('sortType') ?? Config::REPORTS_PAGINATION_DEFAULT_SORT_TYPE;
$page = $request->query->get('page') ?? Config::REPORTS_PAGINATION_DEFAULT_PAGE_NUMBER;
$limit = $request->query->get('limit') ?? Config::REPORTS_PAGINATION_DEFAULT_PAGE_SIZE;
$downloadDataAsCSV = $request->query->get('downloadCSV') == 'true';
$tableColumns = $this->commonCalls->changeColumnVisibilityForTable(
array_values($this->commonCalls->getDataFromJsonFile(Config::JSON_FILE_CLIENT_SIDE_PUBLISHER_TRAFFIC_REPORT)),
$selectedColumns,
[]
);
$finalReportData = $this->mmpComponents->getMmpCumulativeData(
$filters,
$excludedFiltersFlags,
$eventTimestampFrom,
$eventTimeStampTo,
$selectedColumns,
$groupedColumns
);
$finalReportData = $this->mmpComponents->normalizeMmpSource($finalReportData);
foreach ($tableColumns as $key => $value) {
if (isset($value['aggregate']) && $value['aggregate'] == 'sum') {
$num = round(array_sum(array_column($finalReportData, $value['accessor'])), 2);
if ($value['category'] == 'statistics') {
$tableColumns[$key]['Footer'] = number_format($num);
} else {
$tableColumns[$key]['Footer'] = number_format($num, 2);
}
}
}
if ($downloadDataAsCSV) {
$this->commonCalls->downloadCSV($tableColumns, $finalReportData, 'Traffic Report ' . $dateStart . '_' . $dateEnd);
} else {
$finalReportData = array_values($finalReportData);
if (sizeof($finalReportData)) {
$entity = $finalReportData[0];
if (array_key_exists($sortBy, $entity)) {
$sortFlag = 3;
if ($sortType == Config::SORT_TYPE_ASC) {
$sortFlag = 4;
}
array_multisort(array_column($finalReportData, $sortBy), $sortFlag, $finalReportData);
}
}
$offset = $limit * ($page - 1);
$totalRecordCount = sizeof($finalReportData);
$noOfPages = ceil($totalRecordCount / $limit);
return new JsonResponse([
'response' => [
'success' => true,
'httpStatus' => Config::HTTP_STATUS_CODE_OK,
'data' => [
'tableColumns' => $tableColumns,
'data' => array_slice($finalReportData, $offset, $limit),
'metaData' => [
'total' => $totalRecordCount,
'limit' => (int)$limit,
'page' => (int)$page,
'pages' => (int)$noOfPages,
]
],
'error' => null
]
], Config::HTTP_STATUS_CODE_OK);
}
}
/**
* @Route("/table-columns/{table}", name="get_table_columns", methods={"GET"})
*/
public function getTableColumnsAction(Request $request, $table)
{
return new JsonResponse(array_values($this->commonCalls->getDataFromJsonFile(Config::TABLE_COLUMNS_WITH_JSON_FILE[$table])));
}
/**
* @Route("/mobile-apps-list", name="get_mobile_apps_list", methods={"GET"})
*/
public function getMobileAppsListAction(Request $request)
{
$mobileAppsData = $this->doctrine->getRepository(MmpMobileApps::class)->getMmpMobileAppByIsDeleted(0);
$appIds = array_values(array_unique(array_merge(array_column($mobileAppsData, 'bundleId'), [])));
$appData = $this->commonCalls->getAppInfoWithKeys();
$data = [];
foreach ($appIds as $key => $value) {
if (preg_match("/\t/", $value)) {
continue;
}
$appName = array_key_exists($value, $appData) ? " - " . $appData[$value]['title'] : '';
$data[$value] = [
'value' => $value,
'label' => $value . $appName
];
}
return new JsonResponse(array_values($data));
}
/**
* @Route("/countries", name="get_countries", methods={"GET"})
*/
public function getCountriesAction()
{
$countryList = [];
foreach (Config::COUNTRIES as $key => $value) {
$countryList[$key] = [
'value' => $key,
'label' => $key . ' - ' . $value['name']
];
}
ksort($countryList);
return new JsonResponse(array_values($countryList));
}
/**
* @Route("/mafo-affiliates", methods={"GET"})
*/
public
function getMafoAffiliatesAction(Request $request)
{
$affiliateData = $this->getAffiliateAndManagerData();
$mappedAffiliateIds = $affiliateData['mappedAffiliateIds'];
if (empty($mappedAffiliateIds)) {
return new JsonResponse([]);
}
$affiliates = $this->doctrine->getRepository(MafoAffiliates::class)->getAffiliates([], [], Config::REPORTS_PAGINATION_DEFAULT_CSV_PAGE_SIZE, Config::REPORTS_PAGINATION_DEFAULT_PAGE_NUMBER, Config::REPORTS_PAGINATION_DEFAULT_SORT_BY, Config::REPORTS_PAGINATION_DEFAULT_SORT_TYPE);
$arr = [];
foreach ($affiliates as $key => $value) {
if (in_array((string)$value['id'], $mappedAffiliateIds)) {
$arr[] = [
'value' => $value['id'],
'label' => $value['id'] . ' - ' . $value['name']
];
}
}
return new JsonResponse($arr);
}
/**
* @Route("/offers", name="get_offers", methods={"GET"})
*/
public function getPublisherOffersAction(Request $request)
{
$filters = $request->query->all('filters') ?? [];
$excludedFlagForFilters = $request->query->all('excludedFlagForFilters') ?? [];
$publisherInfo = $this->getUser();
$publisherId = $publisherInfo->getId();
$limit = $request->query->get('limit') ? $request->query->get('limit') : Config::REPORTS_PAGINATION_DEFAULT_PAGE_SIZE;
$page = $request->query->get('page') ? $request->query->get('page') : Config::REPORTS_PAGINATION_DEFAULT_PAGE_NUMBER;
$mmpOfferIds = $appIds = $mafoAdvertiserIds = $geos = [];
if (isset($filters)) {
foreach ($filters as $key => $value) {
$key === 'MULTISELECT_MMP_OFFERS' ? $mmpOfferIds = $value : false;
$key === 'MULTISELECT_MMP_STATISTICS_APP' ? $appIds = $value : false;
$key === 'MULTISELECT_MAFO_ADVERTISERS' ? $mafoAdvertiserIds = $value : false;
$key === 'MULTISELECT_GEO' ? $geos = $value : false;
}
}
$mmpOfferIdsExcluded = $appIdsExcluded = $mafoAdvertiserIdsExcluded = $excludeGeos = [];
if (isset($excludedFlagForFilters)) {
foreach ($excludedFlagForFilters as $key => $value) {
$key === 'MULTISELECT_MMP_OFFERS' ? $mmpOfferIdsExcluded = $value : false;
$key === 'MULTISELECT_MMP_STATISTICS_APP' ? $appIdsExcluded = $value : false;
$key === 'MULTISELECT_MAFO_ADVERTISERS' ? $mafoAdvertiserIdsExcluded = $value : false;
$key === 'MULTISELECT_GEO' ? $excludeGeos = $value : false;
}
}
$selectedColumns = $request->query->all('data') != '' ? $request->query->all('data') : [];
$publisherOfferIds = $this->mmpComponents->getVisibleOfferIdsForPublisherManager($publisherId);
$mappedAffiliateIds = $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
// If no offers found for this publisher, return empty result
if (empty($publisherOfferIds)) {
return new JsonResponse([
'response' => [
'success' => true,
'httpStatus' => Config::HTTP_STATUS_CODE_OK,
'data' => [
'tableColumns' => [],
'data' => [],
'metaData' => [
'total' => 0,
'limit' => (int)$limit,
'page' => (int)$page,
'pages' => 0,
]
],
'error' => null
]
], Config::HTTP_STATUS_CODE_OK);
}
// Merge with existing mmpOfferIds filter
if (!empty($mmpOfferIds)) {
$mmpOfferIds = array_intersect($mmpOfferIds, $publisherOfferIds);
} else {
$mmpOfferIds = $publisherOfferIds;
}
// Use the common service to get and process offers data
$filters = [
'MULTISELECT_MMP_OFFERS' => $mmpOfferIds,
'MULTISELECT_MMP_STATISTICS_APP' => $appIds,
'MULTISELECT_MAFO_ADVERTISERS' => $mafoAdvertiserIds,
'MULTISELECT_GEO' => $geos,
];
$excludedFlagForFilters = [
'MULTISELECT_MMP_OFFERS' => $mmpOfferIdsExcluded,
'MULTISELECT_MMP_STATISTICS_APP' => $appIdsExcluded,
'MULTISELECT_MAFO_ADVERTISERS' => $mafoAdvertiserIdsExcluded,
'MULTISELECT_GEO' => $excludeGeos,
];
$offersData = $this->mmpComponents->getMmpOffersData($filters, $excludedFlagForFilters);
$hoAffiliateListList = $this->commonCalls->getAffiliateListByStatusWithKeys();
$processedData = $this->mmpComponents->processOffersData($offersData, $hoAffiliateListList);
$offersData = $processedData['offersData'];
$publisherPermissionsByOfferId = $processedData['publisherPermissionsByOfferId'];
$tableColumns = $this->commonCalls->changeColumnVisibilityForTable(array_values($this->commonCalls->getDataFromJsonFile(Config::JSON_FILE_CLIENT_SIDE_PUBLIHSER_MMP_STATISTICS_MMP_OFFERS)), $selectedColumns, []);
// Pass publisher manager ID to filter allowed/blocked lists to show only managed affiliates
$finalArr = $this->mmpComponents->formatOffersForResponse($offersData, $publisherPermissionsByOfferId, $publisherId);
$finalArr = $this->mmpComponents->normalizeMmpSource($finalArr);
$finalArr = $this->replacePayoutFromPartnerRules($finalArr, $selectedColumns, $mappedAffiliateIds);
$downloadCSV = $request->query->get('downloadCSV');
if ($downloadCSV == 1 || $downloadCSV === 'true' || $downloadCSV === true) {
$this->commonCalls->downloadCSV($tableColumns, $finalArr, 'MMP Offers');
}
$offset = $limit * ($page - 1);
$totalRecordCount = sizeof($finalArr);
$noOfPages = ceil($totalRecordCount / $limit);
return new JsonResponse([
'response' => [
'success' => true,
'httpStatus' => Config::HTTP_STATUS_CODE_OK,
'data' => [
'tableColumns' => $tableColumns,
'data' => array_slice($finalArr, $offset, $limit),
'metaData' => [
'total' => $totalRecordCount,
'limit' => (int)$limit,
'page' => (int)$page,
'pages' => (int)$noOfPages,
]
],
'error' => null
]
], Config::HTTP_STATUS_CODE_OK);
}
/**
* Get visible offers for a Publisher Manager based on new affiliate mapping logic
* @param int $publisherManagerId The Publisher Manager (PAM) ID
* @return array Array of visible offer IDs
*/
/**
* @Route("/mmp-tracking-system", name="get_mmp-tracking-system", methods={"GET"})
*/
public function getMmpTrackingSystem(Request $request)
{
$mmpTrackingSystem = [];
foreach (Config::MMP_TRACKING_SYSTEM_ADVERTISER_CABINET_SIMPLIFIED as $key => $value) {
$mmpTrackingSystem[] = [
'value' => $key,
'label' => $value
];
}
return new JsonResponse($mmpTrackingSystem);
}
/**
* @Route("/mafo-offers", methods={"GET"})
*/
public
function getMafoOffersAction(Request $request)
{
$offers = $this->doctrine->getRepository(MafoOffers::class)->getOffersList();
$arr = [];
foreach ($offers as $key => $value) {
$arr[] = [
'value' => $value['id'],
'label' => $value['id'] . ' - ' . $value['name']
];
}
return new JsonResponse($arr);
}
/**
* @Route("/advertiser-manager", name="get_advertiser_managers", methods={"GET"})
*/
public function getMafoUsersAction(Request $request)
{
// Forward to UtilitiesController logic
return $this->forward('App\Controller\UtilitiesController::getMafoUsersAction', [
'request' => $request,
]);
}
/**
* Replace payout and payoutModelPretty values from mmp_partner_rules table
*
* @param array $finalArr - The formatted offers array
* @param array $selectedColumns - The columns selected by the user
* @param array $mappedAffiliateIds - The affiliate IDs mapped to the publisher
* @return array - The modified offers array with replaced payout values
*/
private function replacePayoutFromPartnerRules(array $finalArr, array $selectedColumns, array $mappedAffiliateIds): array
{
if (empty($finalArr)) {
return $finalArr;
}
$mmpOfferIds = array_unique(array_column($finalArr, 'mmpOfferId'));
if (empty($mmpOfferIds)) {
return $finalArr;
}
$mmpPartnerRulesRepo = $this->doctrine->getRepository(\App\Entity\MmpPartnerRules::class);
$partnerRules = $mmpPartnerRulesRepo->getPayoutRulesByMmpOfferIds($mmpOfferIds);
if (empty($partnerRules)) {
return $finalArr;
}
$payoutMap = [];
foreach ($partnerRules as $rule) {
$mmpOfferId = $rule['mmpOfferId'];
if (!isset($payoutMap[$mmpOfferId])) {
$payoutMap[$mmpOfferId] = [
'payout' => $rule['payout'],
'payoutModel' => $rule['payoutModel']
];
}
}
foreach ($finalArr as &$offer) {
$mmpOfferId = $offer['mmpOfferId'];
if (isset($payoutMap[$mmpOfferId])) {
$partnerPayout = $payoutMap[$mmpOfferId];
if ($partnerPayout['payout'] !== null) {
$offer['payout'] = $partnerPayout['payout'];
}
if ($partnerPayout['payoutModel'] !== null) {
$offer['payoutModelPretty'] = strtoupper($partnerPayout['payoutModel']);
}
}
}
return $finalArr;
}
// ==================== INVOICE CONTROL ENDPOINTS ====================
/**
* Get invoice payment statuses
* @Route("/invoice-control/statuses", name="get_invoice_statuses", methods={"GET"})
*/
public function getInvoiceStatusesAction(Request $request)
{
return new JsonResponse($this->commonCalls->getPublisherCabinetInvoiceStatusOptions());
}
/**
* Get invoices list for the publisher cabinet
* Supports multiselect filters for status and affiliateId
* @Route("/invoice-control", name="get_invoices", methods={"GET"})
*/
public function getInvoicesAction(Request $request)
{
$publisherId = $this->getUser()->getId();
$mappedAffiliateIds = $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
if (empty($mappedAffiliateIds)) {
return new JsonResponse([
'response' => [
'success' => true,
'httpStatus' => Config::HTTP_STATUS_CODE_OK,
'data' => ['data' => [], 'metaData' => ['total' => 0, 'limit' => Config::REPORTS_PAGINATION_DEFAULT_PAGE_SIZE, 'page' => 1, 'pages' => 0]],
'message' => 'No data available.',
'error' => null
]
], Config::HTTP_STATUS_CODE_OK);
}
// Parse filters - support multiselect (arrays) for status and affiliateId
$filters = [];
// Payment Status filter (multiselect): status[0]=open&status[1]=approved
$statusFilter = $request->query->all('status');
if (!empty($statusFilter)) {
$filters['status'] = is_array($statusFilter) ? $statusFilter : [$statusFilter];
}
// Mafo Publisher Id filter (multiselect): affiliateId[0]=55&affiliateId[1]=838
$affiliateIdFilter = $request->query->all('affiliateId');
if (!empty($affiliateIdFilter)) {
// Only allow filtering by mapped affiliate IDs (security check)
$requestedIds = is_array($affiliateIdFilter) ? $affiliateIdFilter : [$affiliateIdFilter];
$validIds = array_intersect(array_map('intval', $requestedIds), array_map('intval', $mappedAffiliateIds));
if (!empty($validIds)) {
$filters['mafoAffiliateId'] = $validIds;
}
}
// Date Range filter
$dateStart = $request->query->get('startDate') ? date('Y-m-d', strtotime($request->query->get('startDate'))) : null;
$dateEnd = $request->query->get('endDate') ? date('Y-m-d', strtotime($request->query->get('endDate'))) : null;
// Pagination
$limit = (int)($request->query->get('limit') ?: Config::REPORTS_PAGINATION_DEFAULT_PAGE_SIZE);
$page = (int)($request->query->get('page') ?: Config::REPORTS_PAGINATION_DEFAULT_PAGE_NUMBER);
// Sorting - map created_at to dateInserted
$sortBy = $request->query->get('sortBy') ?? 'id';
$sortType = strtoupper($request->query->get('sortType') ?? Config::REPORTS_PAGINATION_DEFAULT_SORT_TYPE);
// Map sort field names
$sortFieldMap = ['created_at' => 'dateInserted', 'updated_at' => 'dateUpdated'];
$sortBy = $sortFieldMap[$sortBy] ?? $sortBy;
// Get invoices
$invoices = $this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->getInvoicesByPublisherManagerId($publisherId, $mappedAffiliateIds, $dateStart, $dateEnd, $filters);
// Get affiliate names for display
$affiliateNames = $this->getAffiliateNamesMap($mappedAffiliateIds);
// Format the data with canEdit flag based on status
$formattedInvoices = array_map(function($invoice) use ($affiliateNames) {
$formatted = $this->formatInvoiceForResponse($invoice, $affiliateNames);
// Add canEdit flag - Edit button should be disabled if status is APPROVED
$formatted['canEdit'] = strtolower($invoice['status'] ?? '') !== Config::PUBLISHER_CABINET_INVOICE_STATUS_APPROVED;
return $formatted;
}, $invoices);
// Sorting
if (!empty($formattedInvoices) && isset($formattedInvoices[0][$sortBy])) {
$sortFlag = ($sortType === 'ASC') ? SORT_ASC : SORT_DESC;
array_multisort(array_column($formattedInvoices, $sortBy), $sortFlag, $formattedInvoices);
}
// Pagination
$totalRecordCount = count($formattedInvoices);
$noOfPages = $totalRecordCount > 0 ? ceil($totalRecordCount / $limit) : 0;
$offset = $limit * ($page - 1);
return new JsonResponse([
'response' => [
'success' => true,
'httpStatus' => Config::HTTP_STATUS_CODE_OK,
'data' => [
'data' => array_slice($formattedInvoices, $offset, $limit),
'metaData' => ['total' => $totalRecordCount, 'limit' => $limit, 'page' => $page, 'pages' => (int)$noOfPages]
],
'error' => null
]
], Config::HTTP_STATUS_CODE_OK);
}
/**
* Upload a new invoice
* @Route("/invoice-control", name="post_invoice", methods={"POST"})
*/
public function postInvoiceAction(Request $request)
{
$publisherInfo = $this->getUser();
$publisherId = $publisherInfo->getId();
$mappedAffiliateIds = $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
$errors = [];
// Validate required fields
$mafoAffiliateId = $request->request->get('mafoPartnerId');
$amount = $request->request->get('amount');
$periodInput = $request->request->get('period'); // Format: YYYY-MM (e.g., 2026-01)
$file = $request->files->get('invoiceFile');
// Validation
if (empty($mafoAffiliateId)) {
$errors[] = 'MAFO Partner ID is required.';
} elseif (!in_array((int)$mafoAffiliateId, array_map('intval', $mappedAffiliateIds))) {
$errors[] = 'Invalid MAFO Partner ID. You can only upload invoices for your mapped partners.';
}
if (empty($amount) || !is_numeric($amount) || $amount <= 0) {
$errors[] = 'Amount is required and must be a positive number.';
}
if (empty($periodInput)) {
$errors[] = 'Period (Year-Month) is required.';
} elseif (!preg_match('/^\d{4}-\d{2}$/', $periodInput)) {
$errors[] = 'Period must be in YYYY-MM format.';
}
if (!$file) {
$errors[] = 'Invoice file is required.';
} else {
// Validate file
$fileValidation = $this->validateInvoiceFile($file);
if (!empty($fileValidation)) {
$errors = array_merge($errors, $fileValidation);
}
}
if (!empty($errors)) {
return new JsonResponse([
'response' => [
'success' => false,
'httpStatus' => Config::HTTP_STATUS_CODE_BAD_REQUEST,
'data' => null,
'error' => implode(' ', $errors)
]
], Config::HTTP_STATUS_CODE_BAD_REQUEST);
}
// Upload file to S3 (cabinet-uploads directory for GuardDuty scanning)
$s3Directory = Config::S3_CABINET_UPLOADS_DIRECTORY;
$uploadDate = (new \DateTime())->format('Y-m-d');
$uploadTimestamp = time();
$originalFileName = $file->getClientOriginalName();
$linkToFile = $s3Directory . '/publisher/invoice-' . $publisherId . '-' . $mafoAffiliateId . '-' . $uploadDate . '-' . $originalFileName . '-' . $uploadTimestamp;
$uploadedFileKey = $this->s3->uploadFileToS3($file->getPathname(), $linkToFile);
if (!$uploadedFileKey) {
return new JsonResponse([
'response' => [
'success' => false,
'httpStatus' => 500,
'data' => null,
'error' => 'Failed to upload file. Please try again.'
]
], 500);
}
// Create period datetime (first day of the month)
$period = new \DateTime($periodInput . '-01');
// Create the invoice record
try {
$invoice = $this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->createInvoice(
$publisherId,
(int)$mafoAffiliateId,
$amount,
$period,
$uploadedFileKey,
$request->request->get('currency') ?? Config::CURRENCY_USD
);
// Get affiliate name for response
$affiliateNames = $this->getAffiliateNamesMap([(int)$mafoAffiliateId]);
$formattedInvoice = $this->formatInvoiceForResponse([
'id' => $invoice->getId(),
'mafoPublisherCabinetManagerId' => $invoice->getMafoPublisherCabinetManagerId(),
'mafoAffiliateId' => $invoice->getMafoAffiliateId(),
'amount' => $invoice->getAmount(),
'period' => $invoice->getPeriod(),
'status' => $invoice->getStatus(),
'linkToFile' => $invoice->getLinkToFile(),
'currency' => $invoice->getCurrency(),
'approvedByEmail' => $invoice->getApprovedByEmail(),
'dateApproved' => $invoice->getDateApproved(),
'dateUpdated' => $invoice->getDateUpdated(),
'dateInserted' => $invoice->getDateInserted(),
], $affiliateNames);
return new JsonResponse([
'response' => [
'success' => true,
'httpStatus' => 201,
'data' => $formattedInvoice,
'message' => 'Invoice uploaded successfully.',
'error' => null
]
], 201);
} catch (\Exception $e) {
// Delete the uploaded file if invoice creation failed
$this->s3->deleteFileFromS3($uploadedFileKey);
return new JsonResponse([
'response' => [
'success' => false,
'httpStatus' => 500,
'data' => null,
'error' => 'Failed to create invoice record. Please try again.'
]
], 500);
}
}
/**
* Update an existing invoice (only amount and file editable, not for APPROVED status)
* @Route("/invoice-control/{id}", name="put_invoice", methods={"POST", "PUT", "PATCH"})
*/
public function updateInvoiceAction(Request $request, int $id)
{
$publisherId = $this->getUser()->getId();
$mappedAffiliateIds = $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
$invoice = $this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->getInvoiceByIdAndPublisher($id, $publisherId, $mappedAffiliateIds);
if (!$invoice) {
return new JsonResponse([
'response' => ['success' => false, 'httpStatus' => 404, 'data' => null, 'error' => 'Invoice not found.']
], 404);
}
if (!$this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->canEditInvoice($id)) {
return new JsonResponse([
'response' => ['success' => false, 'httpStatus' => Config::HTTP_STATUS_CODE_FORBIDDEN, 'data' => null, 'error' => 'Cannot edit an approved invoice.']
], Config::HTTP_STATUS_CODE_FORBIDDEN);
}
// Parse request data based on content type
$contentType = $request->headers->get('Content-Type', '');
$method = $request->getMethod();
if (strpos($contentType, 'application/json') !== false) {
$requestData = json_decode($request->getContent(), true) ?? [];
$amount = $requestData['amount'] ?? null;
$file = null;
} elseif (strpos($contentType, 'multipart/form-data') !== false && in_array($method, ['PUT', 'PATCH'])) {
$parsedData = $this->parseMultipartFormData($request);
$amount = $parsedData['fields']['amount'] ?? null;
$file = $parsedData['files']['invoiceFile'] ?? null;
} else {
$amount = $request->request->get('amount');
$file = $request->files->get('invoiceFile');
}
$errors = [];
$dataToUpdate = [];
// Validate amount
if ($amount !== null && $amount !== '') {
if (!is_numeric($amount) || $amount <= 0) {
$errors[] = 'Amount must be a positive number.';
} else {
$dataToUpdate['amount'] = $amount;
}
}
// Handle file update
if ($file) {
$fileValidation = $this->validateInvoiceFile($file);
if (!empty($fileValidation)) {
$errors = array_merge($errors, $fileValidation);
} else {
if (!empty($invoice['linkToFile'])) {
$this->s3->deleteFileFromS3($invoice['linkToFile']);
}
$filePath = is_array($file) ? $file['tmp_name'] : $file->getPathname();
$fileName = is_array($file) ? $file['name'] : $file->getClientOriginalName();
$fileKey = Config::S3_CABINET_UPLOADS_DIRECTORY . '/' . $publisherId . '/' . time() . '-' . $fileName;
$uploadedFileKey = $this->s3->uploadFileToS3($filePath, $fileKey);
if ($uploadedFileKey) {
$dataToUpdate['linkToFile'] = $uploadedFileKey;
} else {
$errors[] = 'Failed to upload file.';
}
}
}
if (!empty($errors)) {
return new JsonResponse([
'response' => ['success' => false, 'httpStatus' => Config::HTTP_STATUS_CODE_BAD_REQUEST, 'data' => null, 'error' => implode(' ', $errors)]
], Config::HTTP_STATUS_CODE_BAD_REQUEST);
}
if (empty($dataToUpdate)) {
return new JsonResponse([
'response' => ['success' => false, 'httpStatus' => Config::HTTP_STATUS_CODE_BAD_REQUEST, 'data' => null, 'error' => 'No data to update.']
], Config::HTTP_STATUS_CODE_BAD_REQUEST);
}
$updatedInvoice = $this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->updateInvoice($id, $dataToUpdate);
if (!$updatedInvoice) {
return new JsonResponse([
'response' => ['success' => false, 'httpStatus' => 500, 'data' => null, 'error' => 'Failed to update invoice.']
], 500);
}
$affiliateNames = $this->getAffiliateNamesMap([$updatedInvoice->getMafoAffiliateId()]);
$formattedInvoice = $this->formatInvoiceForResponse([
'id' => $updatedInvoice->getId(),
'mafoPublisherCabinetManagerId' => $updatedInvoice->getMafoPublisherCabinetManagerId(),
'mafoAffiliateId' => $updatedInvoice->getMafoAffiliateId(),
'amount' => $updatedInvoice->getAmount(),
'period' => $updatedInvoice->getPeriod(),
'status' => $updatedInvoice->getStatus(),
'linkToFile' => $updatedInvoice->getLinkToFile(),
'currency' => $updatedInvoice->getCurrency(),
'approvedByEmail' => $updatedInvoice->getApprovedByEmail(),
'dateApproved' => $updatedInvoice->getDateApproved(),
'dateUpdated' => $updatedInvoice->getDateUpdated(),
'dateInserted' => $updatedInvoice->getDateInserted(),
], $affiliateNames);
return new JsonResponse([
'response' => ['success' => true, 'httpStatus' => Config::HTTP_STATUS_CODE_OK, 'data' => $formattedInvoice, 'message' => 'Invoice updated successfully.', 'error' => null]
], Config::HTTP_STATUS_CODE_OK);
}
/**
* Parse multipart form data for PUT/PATCH requests
*/
private function parseMultipartFormData(Request $request): array
{
$result = ['fields' => [], 'files' => []];
$contentType = $request->headers->get('Content-Type', '');
if (!preg_match('/boundary=(.*)$/i', $contentType, $matches)) {
return $result;
}
$parts = preg_split('/-+' . preg_quote($matches[1], '/') . '/', $request->getContent());
foreach ($parts as $part) {
if (empty(trim($part)) || $part === '--') continue;
$segments = preg_split('/\r\n\r\n/', $part, 2);
if (count($segments) < 2) continue;
$headers = $segments[0];
$body = rtrim($segments[1], "\r\n");
if (!preg_match('/Content-Disposition:.*name="([^"]+)"(?:.*filename="([^"]+)")?/i', $headers, $matches)) continue;
if (isset($matches[2])) {
$tmpFile = tempnam(sys_get_temp_dir(), 'upload_');
file_put_contents($tmpFile, $body);
$mimeType = preg_match('/Content-Type:\s*([^\r\n]+)/i', $headers, $typeMatch) ? trim($typeMatch[1]) : 'application/octet-stream';
$result['files'][$matches[1]] = ['name' => $matches[2], 'type' => $mimeType, 'tmp_name' => $tmpFile, 'error' => UPLOAD_ERR_OK, 'size' => strlen($body)];
} else {
$result['fields'][$matches[1]] = $body;
}
}
return $result;
}
/**
* Download invoice file
* @Route("/invoice-control/{id}/download", name="download_invoice_file", methods={"GET"})
*/
public function downloadInvoiceFileAction(Request $request, int $id)
{
$publisherInfo = $this->getUser();
$publisherId = $publisherInfo->getId();
$mappedAffiliateIds = $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
// Check if invoice exists and belongs to this publisher
$invoice = $this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->getInvoiceByIdAndPublisher($id, $publisherId, $mappedAffiliateIds);
if (!$invoice) {
return new JsonResponse([
'response' => [
'success' => false,
'httpStatus' => 404,
'data' => null,
'error' => 'Invoice not found.'
]
], 404);
}
if (empty($invoice['linkToFile'])) {
return new JsonResponse([
'response' => [
'success' => false,
'httpStatus' => 404,
'data' => null,
'error' => 'Invoice file not found.'
]
], 404);
}
// Get file from S3
$s3Directory = Config::S3_CABINET_UPLOADS_DIRECTORY;
$fileBody = $this->s3->getFileFromS3($s3Directory, $invoice['linkToFile']);
return new Response($fileBody, 200);
}
/**
* Get single invoice by ID
* @Route("/invoice-control/{id}", name="get_invoice", methods={"GET"})
*/
public function getInvoiceAction(Request $request, int $id)
{
$publisherInfo = $this->getUser();
$publisherId = $publisherInfo->getId();
$mappedAffiliateIds = $this->mappingRepository->findMappedAffiliateIdsByPublisherId($publisherId);
// Check if invoice exists and belongs to this publisher
$invoice = $this->doctrine->getRepository(MafoPublisherCabinetInvoice::class)->getInvoiceByIdAndPublisher($id, $publisherId, $mappedAffiliateIds);
if (!$invoice) {
return new JsonResponse([
'response' => [
'success' => false,
'httpStatus' => 404,
'data' => null,
'error' => 'Invoice not found.'
]
], 404);
}
// Get affiliate names for display
$affiliateNames = $this->getAffiliateNamesMap([$invoice['mafoAffiliateId']]);
$formattedInvoice = $this->formatInvoiceForResponse($invoice, $affiliateNames);
return new JsonResponse([
'response' => [
'success' => true,
'httpStatus' => Config::HTTP_STATUS_CODE_OK,
'data' => $formattedInvoice,
'error' => null
]
], Config::HTTP_STATUS_CODE_OK);
}
// ==================== INVOICE CONTROL HELPER METHODS ====================
/**
* Validate uploaded invoice file
* Handles both UploadedFile objects and array format from manual parsing
*/
private function validateInvoiceFile($file): array
{
$errors = [];
// Get file properties - handle both UploadedFile and array format
if (is_array($file)) {
$fileSize = $file['size'] ?? 0;
$fileName = $file['name'] ?? '';
$mimeType = $file['type'] ?? '';
$extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
} else {
$fileSize = $file->getSize();
$fileName = $file->getClientOriginalName();
$mimeType = $file->getMimeType();
$extension = strtolower($file->getClientOriginalExtension());
}
// Check file size (10MB max)
$maxFileSize = Config::PUBLISHER_CABINET_INVOICE_MAX_FILE_SIZE_BYTES;
if ($fileSize > $maxFileSize) {
$errors[] = 'File size exceeds maximum allowed size of ' . Config::PUBLISHER_CABINET_INVOICE_MAX_FILE_SIZE_MB . 'MB.';
}
// Check file extension
$allowedExtensions = Config::PUBLISHER_CABINET_INVOICE_ALLOWED_FILE_TYPES;
if (!in_array($extension, $allowedExtensions)) {
$errors[] = 'Unsupported file format. Allowed formats: ' . implode(', ', $allowedExtensions) . '.';
}
// Check MIME type
$allowedMimeTypes = [
'application/pdf',
];
if (!in_array($mimeType, $allowedMimeTypes)) {
$errors[] = 'Invalid file type. Please upload a valid document or image file.';
}
return $errors;
}
/**
* Get affiliate names mapped by ID
*/
private function getAffiliateNamesMap(array $affiliateIds): array
{
if (empty($affiliateIds)) {
return [];
}
$affiliates = $this->doctrine->getRepository(MafoAffiliates::class)->findBy(['id' => $affiliateIds]);
$map = [];
foreach ($affiliates as $affiliate) {
$map[$affiliate->getId()] = $affiliate->getName();
}
return $map;
}
/**
* Format invoice data for API response
*/
private function formatInvoiceForResponse(array $invoice, array $affiliateNames): array
{
$period = $invoice['period'];
$periodPretty = $period instanceof \DateTimeInterface ? $period->format('M Y') : '';
$dateApproved = $invoice['dateApproved'] ?? null;
$dateApprovedPretty = $dateApproved instanceof \DateTimeInterface ? $dateApproved->format('Y-m-d H:i:s') : '';
$dateInserted = $invoice['dateInserted'] ?? null;
$dateInsertedPretty = $dateInserted instanceof \DateTimeInterface ? $dateInserted->format('Y-m-d H:i:s') : '';
$dateUpdated = $invoice['dateUpdated'] ?? null;
$dateUpdatedPretty = $dateUpdated instanceof \DateTimeInterface ? $dateUpdated->format('Y-m-d H:i:s') : '';
$status = $invoice['status'] ?? '';
$statusLabel = Config::PUBLISHER_CABINET_INVOICE_STATUS_LABELS[$status] ?? $status;
$canEdit = $status !== Config::PUBLISHER_CABINET_INVOICE_STATUS_APPROVED;
return [
'id' => $invoice['id'],
'mafoPublisherCabinetManagerId' => $invoice['mafoPublisherCabinetManagerId'],
'mafoAffiliateId' => $invoice['mafoAffiliateId'],
'affiliateName' => $affiliateNames[$invoice['mafoAffiliateId']] ?? 'Unknown',
'amount' => number_format((float)$invoice['amount'], 2),
'amountRaw' => $invoice['amount'],
'currency' => $invoice['currency'] ?? Config::CURRENCY_USD,
'period' => $period instanceof \DateTimeInterface ? $period->format('Y-m-d') : '',
'periodPretty' => $periodPretty,
'status' => $status,
'statusLabel' => $statusLabel,
'linkToFile' => $invoice['linkToFile'] ?? '',
'approvedByEmail' => $invoice['approvedByEmail'] ?? '',
'dateApproved' => $dateApproved instanceof \DateTimeInterface ? $dateApproved->format('Y-m-d H:i:s') : '',
'dateApprovedPretty' => $dateApprovedPretty,
'dateInserted' => $dateInserted instanceof \DateTimeInterface ? $dateInserted->format('Y-m-d H:i:s') : '',
'dateInsertedPretty' => $dateInsertedPretty,
'dateUpdated' => $dateUpdated instanceof \DateTimeInterface ? $dateUpdated->format('Y-m-d H:i:s') : '',
'dateUpdatedPretty' => $dateUpdatedPretty,
'canEdit' => $canEdit,
];
}
}