src/UI/Admin/Controller/PaymentController.php line 312

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\UI\Admin\Controller;
  4. use App\Domain\Payment\Model\Payment;
  5. use App\UI\Admin\Datatable\PaymentDatatable;
  6. use App\UI\Shared\Controller\AbstractController;
  7. use Doctrine\Persistence\ManagerRegistry;
  8. use Sg\DatatablesBundle\Response\DatatableResponse;
  9. use Symfony\Component\HttpFoundation\Request;
  10. use Symfony\Component\HttpFoundation\Response;
  11. use Symfony\Component\HttpFoundation\StreamedResponse;
  12. use Symfony\Component\Routing\Annotation\Route;
  13. use Symfony\Component\HttpFoundation\JsonResponse;
  14. use Doctrine\DBAL\Connection;
  15. use Symfony\Component\HttpFoundation\ResponseHeaderBag;
  16. use Psr\Log\LoggerAwareInterface;
  17. use Psr\Log\LoggerAwareTrait;
  18. #[Route('/payments')]
  19. class PaymentController extends AbstractController implements LoggerAwareInterface
  20. {
  21.     use LoggerAwareTrait;
  22.     #[Route(path'/'name'admin_payments')]
  23.     public function index(
  24.         Request $request,
  25.         PaymentDatatable $datatable,
  26.         DatatableResponse $datatableResponse,
  27.         \Doctrine\Persistence\ManagerRegistry $doctrine,
  28.     ): Response {
  29.         $isAjax $request->isXmlHttpRequest();
  30.         $datatable->buildDatatable();
  31.         if ($isAjax) {
  32.             try {
  33.                 // Log raw incoming params
  34.                 // $this->logger->info('Request query parameters', $request->query->all());
  35.                 $data $request->query->all();
  36.                 $tzName $request->query->get('tz') ?? \date_default_timezone_get() ?? 'UTC';
  37.                 // Validate/normalize user tz
  38.                 try {
  39.                     $userTz = new \DateTimeZone($tzName);
  40.                 } catch (\Throwable) {
  41.                     $tzName 'UTC';
  42.                     $userTz = new \DateTimeZone('UTC');
  43.                 }
  44.                 // $this->logger->info('TIMEZONE', ['tz' => $tzName]);
  45.                 // -------- Detect DB session timezone (MySQL / Postgres) --------
  46.                 $conn $doctrine->getConnection();
  47.                 $dbTzName 'UTC';
  48.                 try {
  49.                     $platform $conn->getDatabasePlatform()->getName();
  50.                     if ($platform === 'mysql') {
  51.                         $sessionTz $conn->fetchOne('SELECT @@time_zone');
  52.                         if ($sessionTz === 'SYSTEM') {
  53.                             $systemTz $conn->fetchOne('SELECT @@system_time_zone');
  54.                             $dbTzName $systemTz ?: 'UTC';
  55.                         } else {
  56.                             $dbTzName $sessionTz ?: 'UTC';
  57.                         }
  58.                     } elseif ($platform === 'postgresql') {
  59.                         // SHOW TIME ZONE returns a single row, single column
  60.                         $dbTzName $conn->fetchOne('SHOW TIME ZONE') ?: 'UTC';
  61.                     }
  62.                 } catch (\Throwable $e) {
  63.                     // keep default 'UTC' on any failure
  64.                 }
  65.                 // Ensure it's a valid IANA/offset for PHP
  66.                 try {
  67.                     $dbTz = new \DateTimeZone($dbTzName);
  68.                 } catch (\Throwable) {
  69.                     $dbTzName 'UTC';
  70.                     $dbTz = new \DateTimeZone('UTC');
  71.                 }
  72.                 // $this->logger->info('DB TIMEZONE', ['db_tz' => $dbTzName, 'platform' => $conn->getDatabasePlatform()->getName()]);
  73.                 // -------- Strip createdAt LIKE filter --------
  74.                 $createdAtRange null;
  75.                 if (isset($data['columns'])) {
  76.                     foreach ($data['columns'] as &$column) {
  77.                         if (($column['data'] ?? null) === 'createdAt' && !empty($column['search']['value'])) {
  78.                             $createdAtRange explode('|'$column['search']['value']);
  79.                             $column['search']['value'] = '';
  80.                             $column['searchable'] = 'false';
  81.                         }
  82.                     }
  83.                     unset($column);
  84.                 }
  85.                 // Overwrite cleaned query
  86.                 $request->query->replace($data);
  87.                 // -------- Build query --------
  88.                 $datatableResponse->setDatatable($datatable);
  89.                 $queryBuilder $datatableResponse->getDatatableQueryBuilder()->getQb();
  90.                 $alias $queryBuilder->getRootAliases()[0];
  91.                 // Always show latest records at the top
  92.                 $queryBuilder->addOrderBy("$alias.createdAt""DESC");
  93.                 if ($createdAtRange && count($createdAtRange) === 2) {
  94.                     $startStr \trim($createdAtRange[0]) . ' 00:00:00';
  95.                     $endStr \trim($createdAtRange[1]) . ' 23:59:59';
  96.                     // Interpret the picked dates as *user-local calendar days*
  97.                     $startLocal \DateTimeImmutable::createFromFormat('Y-m-d H:i:s'$startStr$userTz);
  98.                     $endLocal \DateTimeImmutable::createFromFormat('Y-m-d H:i:s'$endStr$userTz);
  99.                     if (!$startLocal || !$endLocal) {
  100.                         throw new \RuntimeException('Invalid createdAt date range.');
  101.                     }
  102.                     // Convert to *DB session timezone* we detected above
  103.                     $startDb $startLocal->setTimezone($dbTz);
  104.                     $endDb $endLocal->setTimezone($dbTz);
  105.                     $queryBuilder
  106.                         ->andWhere("$alias.createdAt BETWEEN :start AND :end")
  107.                         ->setParameter('start'$startDb'datetime_immutable')
  108.                         ->setParameter('end'$endDb'datetime_immutable');
  109.                     $this->logger->info('createdAt filter applied', [
  110.                         'user_tz' => $tzName,
  111.                         'db_tz' => $dbTzName,
  112.                         'start_loc' => $startLocal->format('Y-m-d H:i:sP'),
  113.                         'end_loc' => $endLocal->format('Y-m-d H:i:sP'),
  114.                         'start_db' => $startDb->format('Y-m-d H:i:sP'),
  115.                         'end_db' => $endDb->format('Y-m-d H:i:sP'),
  116.                     ]);
  117.                 }
  118.                 // -------- Return DataTables JSON --------
  119.                 $response $datatableResponse->getResponse();
  120.                 // $this->logger->info('Payment datatable request completed.', [
  121.                 //     'response' => json_decode($response->getContent(), true)
  122.                 // ]);
  123.                 return $response;
  124.             } catch (\Throwable $e) {
  125.                 return new JsonResponse([
  126.                     'error' => true,
  127.                     'message' => $e->getMessage(),
  128.                     'trace' => $e->getTraceAsString(),
  129.                 ]);
  130.             }
  131.         }
  132.         return $this->render('admin/payment/index.html.twig', [
  133.             'datatable' => $datatable,
  134.         ]);
  135.     }
  136.     #[Route(path'/export-all'name'admin_payments_export_all')]
  137.     public function exportAll(Request $requestManagerRegistry $doctrine): StreamedResponse
  138.     {
  139.         $startYmd $request->query->get('start');
  140.         $endYmd $request->query->get('end');   // e.g. "2025-03-15"
  141.         $tzName $request->query->get('tz') ?? \date_default_timezone_get() ?? 'UTC';
  142.         // --- Validate user tz (fallback to UTC) ---
  143.         try {
  144.             $userTz = new \DateTimeZone($tzName);
  145.         } catch (\Throwable) {
  146.             $tzName 'UTC';
  147.             $userTz = new \DateTimeZone('UTC');
  148.         }
  149.         // --- Detect DB session timezone ---
  150.         $conn $doctrine->getConnection();
  151.         $dbTzName 'UTC';
  152.         try {
  153.             $platform $conn->getDatabasePlatform()->getName();
  154.             if ($platform === 'mysql') {
  155.                 $sessionTz $conn->fetchOne('SELECT @@time_zone');
  156.                 if ($sessionTz === 'SYSTEM') {
  157.                     $systemTz $conn->fetchOne('SELECT @@system_time_zone');
  158.                     $dbTzName $systemTz ?: 'UTC';
  159.                 } else {
  160.                     $dbTzName $sessionTz ?: 'UTC';
  161.                 }
  162.             } elseif ($platform === 'postgresql') {
  163.                 $dbTzName $conn->fetchOne('SHOW TIME ZONE') ?: 'UTC';
  164.             }
  165.         } catch (\Throwable) {
  166.             // keep default UTC
  167.         }
  168.         try {
  169.             $dbTz = new \DateTimeZone($dbTzName);
  170.         } catch (\Throwable) {
  171.             $dbTzName 'UTC';
  172.             $dbTz = new \DateTimeZone('UTC');
  173.         }
  174.         $repo $doctrine->getRepository(Payment::class);
  175.         $qb $repo->createQueryBuilder('p');
  176.         // join user_payment_info
  177.         $qb->leftJoin('p.userPaymentInfo''upi')
  178.             ->addSelect('upi');
  179.         // --- Apply timezone-aware date filter if provided ---
  180.         if ($startYmd && $endYmd) {
  181.             $startLocal \DateTimeImmutable::createFromFormat('Y-m-d H:i:s'trim($startYmd) . ' 00:00:00'$userTz);
  182.             $endLocal \DateTimeImmutable::createFromFormat('Y-m-d H:i:s'trim($endYmd) . ' 23:59:59'$userTz);
  183.             if (!$startLocal || !$endLocal) {
  184.                 throw new \RuntimeException('Invalid start/end date.');
  185.             }
  186.             $startDb $startLocal->setTimezone($dbTz);
  187.             $endDb $endLocal->setTimezone($dbTz);
  188.             $qb->andWhere('p.createdAt BETWEEN :start AND :end')
  189.                 ->setParameter('start'$startDb'datetime_immutable')
  190.                 ->setParameter('end'$endDb'datetime_immutable');
  191.         }
  192.         $qb->orderBy('p.createdAt''DESC');
  193.         $payments $qb->getQuery()->getResult();
  194.         // --- Stream CSV; render "Created At" in the USER'S timezone ---
  195.         $response = new StreamedResponse(function () use ($payments$userTz) {
  196.             $handle fopen('php://output''w+');
  197.             // Header
  198.             fputcsv($handle, ['ID''External ID''Site''Payment Provider''IP Address''Name''PISP Status''Bank Status''Amount''Currency''Created At']);
  199.             // Rows
  200.             /** @var \App\Domain\Payment\Model\Payment $payment */
  201.             foreach ($payments as $payment) {
  202.                 $createdAt $payment->getCreatedAt();
  203.                 $createdAtStr $createdAt
  204.                     ? (new \DateTimeImmutable('@' $createdAt->getTimestamp()))
  205.                         ->setTimezone($userTz)
  206.                         ->format('Y-m-d H:i:s')
  207.                     : '';
  208.                 //ADDING BELOW CODE, BECAUSE PAYMENT PROVIDER, PISP STATUS AND BANK STATUS ARE NOT SET IN FOR OLD PAYMENT ENTITY
  209.                 $paymentProvider '';
  210.                 try {
  211.                     $tmp $payment->getSite()?->getPaymentGatewayConfig()?->getFactoryName();
  212.                     if (is_string($tmp) && $tmp !== '') {
  213.                         $paymentProvider $tmp;
  214.                     }
  215.                 } catch (\Throwable $e) {
  216.                     // ignore errors for old data
  217.                 }
  218.                 $pispStatus '';
  219.                 try {
  220.                     $tmp $payment->getPispStatus();
  221.                     if (is_string($tmp) && $tmp !== '') {
  222.                         $pispStatus $tmp;
  223.                     }
  224.                 } catch (\Throwable $e) {
  225.                 }
  226.                 $bankStatus '';
  227.                 try {
  228.                     $tmp $payment->getBankStatus();
  229.                     if (is_string($tmp) && $tmp !== '') {
  230.                         $bankStatus $tmp;
  231.                     }
  232.                 } catch (\Throwable $e) {
  233.                 }
  234.                 $upi $payment->getUserPaymentInfo();
  235.                 $ip $upi $upi->getIpAddress() : null;
  236.                 $name $upi $upi->getName() : null;
  237.                 fputcsv($handle, [
  238.                     $payment->getId(),
  239.                     $payment->getExternalId(),
  240.                     $payment->getSite()?->getName(),
  241.                     // $payment->getStatus()?->getValue(),
  242.                     $paymentProvider,
  243.                     $ip,
  244.                     $name,
  245.                     $pispStatus,
  246.                     $bankStatus,
  247.                     $payment->getAmount() / 100,
  248.                     $payment->getCurrency(),
  249.                     $createdAtStr,
  250.                 ]);
  251.             }
  252.             fclose($handle);
  253.         });
  254.         $filename 'payments_' date('Ymd_His') . '.csv';
  255.         $response->headers->set('Content-Type''text/csv');
  256.         $response->headers->set('Content-Disposition''attachment; filename="' $filename '"');
  257.         return $response;
  258.     }
  259.     #[Route(path'/payment-logs'name'admin_payment_logs')]
  260.     public function paymentLogs(Request $requestManagerRegistry $doctrine): StreamedResponse
  261.     {
  262.         $paymentId $request->query->get('payment_id');
  263.         if (!$paymentId) {
  264.             throw new \InvalidArgumentException("payment_id is required");
  265.         }
  266.         $conn $doctrine->getConnection();
  267.         $query = <<<SQL
  268.         SELECT id, payment_id, status_code, timestamp, logs, created_at, updated_at
  269.         FROM payment_logs
  270.         WHERE payment_id = :paymentId
  271.         ORDER BY timestamp DESC
  272.     SQL;
  273.         $stmt $conn->prepare($query);
  274.         $result $stmt->executeQuery(['paymentId' => $paymentId]);
  275.         $rows $result->fetchAllAssociative();
  276.         // Helper to decode possibly-double-encoded JSON into an array.
  277.         $decodeMaybe = static function ($valueint $maxPasses 3): array {
  278.             if (is_array($value)) {
  279.                 return $value;
  280.             }
  281.             if (!is_string($value) || $value === '') {
  282.                 return [];
  283.             }
  284.             $s $value;
  285.             for ($i 0$i $maxPasses$i++) {
  286.                 $decoded json_decode($strue);
  287.                 if (json_last_error() === JSON_ERROR_NONE) {
  288.                     if (is_array($decoded)) {
  289.                         return $decoded;               // got the object/array
  290.                     }
  291.                     if (is_string($decoded)) {
  292.                         $s $decoded;                 // it was a JSON string containing JSON — try again
  293.                         continue;
  294.                     }
  295.                     // scalar/null → fall through to cleanup
  296.                 }
  297.                 // Cleanup pass: trim, strip wrapping quotes, unescape backslashes
  298.                 $s trim($s);
  299.                 $len strlen($s);
  300.                 if ($len >= 2) {
  301.                     $first $s[0];
  302.                     $last $s[$len 1];
  303.                     if (($first === '"' && $last === '"') || ($first === "'" && $last === "'")) {
  304.                         $s substr($s1, -1);
  305.                     }
  306.                 }
  307.                 $s stripslashes($s);
  308.             }
  309.             return [];
  310.         };
  311.         $responseData = [];
  312.         foreach ($rows as $log) {
  313.             $logsArray $decodeMaybe($log['logs'] ?? '');
  314.             // Normalize details: if it's a JSON string, decode it once.
  315.             $details $logsArray['details'] ?? '';
  316.             if (is_string($details) && $details !== '') {
  317.                 $maybe json_decode($detailstrue);
  318.                 if (json_last_error() === JSON_ERROR_NONE && is_array($maybe)) {
  319.                     $details $maybe;
  320.                 }
  321.             }
  322.             $responseData[] = [
  323.                 'title' => $logsArray['title'] ?? '',
  324.                 'details' => $details,
  325.                 'timestamp' => $log['timestamp'],
  326.                 'status_code' => $log['status_code'],
  327.             ];
  328.         }
  329.         return new StreamedResponse(function () use ($responseData) {
  330.             echo json_encode($responseDataJSON_PRETTY_PRINT JSON_UNESCAPED_UNICODE JSON_UNESCAPED_SLASHES);
  331.         }, 200, [
  332.             'Content-Type' => 'application/json',
  333.         ]);
  334.     }
  335. }