src/UI/Admin/Controller/RecurringPaymentController.php line 63

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\UI\Admin\Controller;
  4. use App\Domain\Payment\Model\RecurringMandateStatus;
  5. use App\UI\Shared\Controller\AbstractController;
  6. use Doctrine\DBAL\Connection;
  7. use Doctrine\DBAL\ParameterType;
  8. use Doctrine\Persistence\ManagerRegistry;
  9. use Symfony\Component\HttpFoundation\JsonResponse;
  10. use Symfony\Component\HttpFoundation\Request;
  11. use Symfony\Component\HttpFoundation\Response;
  12. use Symfony\Component\HttpFoundation\StreamedResponse;
  13. use Symfony\Component\Routing\Annotation\Route;
  14. #[Route('/recurring-payments')]
  15. class RecurringPaymentController extends AbstractController
  16. {
  17.     // Keyed by the actual <table> column index (see index.html.twig), not a sequential index -
  18.     // DataTables sends the real column position, and column 0/3/5/7/8/10 aren't orderable.
  19.     private const SORTABLE_COLUMNS = [
  20.         1 => 'rm.mandate_ref',
  21.         2 => 's.name',
  22.         4 => 'rpp.amount',
  23.         6 => 'rm.status',
  24.         8 => 'last_payment_at',
  25.         9 => 'rm.created_at',
  26.     ];
  27.     private const MANDATE_LIST_SELECT = "
  28.         rm.id,
  29.         rm.mandate_ref,
  30.         rm.debitor_name,
  31.         rm.debitor_iban,
  32.         rm.debitor_email,
  33.         rm.status,
  34.         rm.created_at,
  35.         s.name AS site_name,
  36.         rpp.currency,
  37.         rpp.amount,
  38.         rpp.interval_value,
  39.         rpp.interval_unit,
  40.         rpp.max_count,
  41.         (SELECT COUNT(*) FROM recurring_payments rp WHERE rp.mandate_id = rm.id::text) AS payments_count,
  42.         (SELECT MAX(rp.created_at::bigint) FROM recurring_payments rp WHERE rp.mandate_id = rm.id::text) AS last_payment_at
  43.     ";
  44.     private const MANDATE_LIST_FROM = '
  45.         FROM recurring_mandates rm
  46.         LEFT JOIN sites s ON s.id::text = rm.site_id
  47.         LEFT JOIN LATERAL (
  48.             SELECT currency, amount, interval_value, interval_unit, max_count
  49.             FROM recurring_payment_plans
  50.             WHERE mandate_id = rm.id::text
  51.             ORDER BY created_at::bigint DESC
  52.             LIMIT 1
  53.         ) rpp ON true
  54.     ';
  55.     #[Route(path: '/', name: 'admin_recurring_payments')]
  56.     public function index(): Response
  57.     {
  58.         return $this->render('admin/recurring_payment/index.html.twig');
  59.     }
  60.     #[Route(path: '/list', name: 'admin_recurring_payments_list', methods: ['GET'])]
  61.     public function list(Request $request, ManagerRegistry $doctrine): JsonResponse
  62.     {
  63.         $conn = $doctrine->getConnection();
  64.         $draw = (int) $request->query->get('draw', 1);
  65.         $start = max(0, (int) $request->query->get('start', 0));
  66.         $length = (int) $request->query->get('length', 10);
  67.         $length = $length > 0 ? min($length, 100) : 10;
  68.         $search = trim((string) $request->query->get('search', ''));
  69.         $orderColumnIndex = (int) $request->query->get('order_column', 9);
  70.         $orderDir = strtolower((string) $request->query->get('order_dir', 'desc')) === 'asc' ? 'ASC' : 'DESC';
  71.         $orderColumn = self::SORTABLE_COLUMNS[$orderColumnIndex] ?? 'rm.created_at';
  72.         $orderExpr = $orderColumn === 'rm.created_at' ? 'rm.created_at::bigint' : $orderColumn;
  73.         $where = '';
  74.         $params = [];
  75.         if ($search !== '') {
  76.             $where = 'WHERE rm.mandate_ref ILIKE :search
  77.                 OR rm.debitor_name ILIKE :search
  78.                 OR rm.debitor_email ILIKE :search
  79.                 OR rm.debitor_iban ILIKE :search
  80.                 OR rm.status ILIKE :search
  81.                 OR s.name ILIKE :search';
  82.             $params['search'] = '%' . $search . '%';
  83.         }
  84.         $baseSql = self::MANDATE_LIST_FROM . $where;
  85.         $recordsTotal = (int) $conn->fetchOne('SELECT COUNT(*) FROM recurring_mandates');
  86.         $recordsFiltered = (int) $conn->fetchOne("SELECT COUNT(*) $baseSql", $params);
  87.         $rows = $conn->fetchAllAssociative(
  88.             'SELECT ' . self::MANDATE_LIST_SELECT . "
  89.             $baseSql
  90.             ORDER BY $orderExpr $orderDir
  91.             LIMIT :length OFFSET :start",
  92.             $params + ['length' => $length, 'start' => $start],
  93.             ['length' => ParameterType::INTEGER, 'start' => ParameterType::INTEGER]
  94.         );
  95.         return new JsonResponse([
  96.             'draw' => $draw,
  97.             'recordsTotal' => $recordsTotal,
  98.             'recordsFiltered' => $recordsFiltered,
  99.             'data' => $rows,
  100.         ]);
  101.     }
  102.     #[Route(path: '/export-all', name: 'admin_recurring_payments_export_all')]
  103.     public function exportAll(ManagerRegistry $doctrine): StreamedResponse
  104.     {
  105.         $conn = $doctrine->getConnection();
  106.         $rows = $conn->fetchAllAssociative(
  107.             'SELECT ' . self::MANDATE_LIST_SELECT . self::MANDATE_LIST_FROM . '
  108.              ORDER BY rm.created_at::bigint DESC'
  109.         );
  110.         $response = new StreamedResponse(function () use ($rows) {
  111.             $handle = fopen('php://output', 'w+');
  112.             fputcsv($handle, [
  113.                 'Mandate Ref', 'Site', 'Debtor Name', 'Debtor Email', 'Debtor IBAN',
  114.                 'Amount', 'Currency', 'Frequency', 'Status',
  115.                 'Payments', 'Payments Total', 'Last Payment', 'Created At',
  116.             ]);
  117.             foreach ($rows as $row) {
  118.                 $total = $row['max_count'] !== null ? (1 + (int) $row['max_count']) : null;
  119.                 fputcsv($handle, [
  120.                     $row['mandate_ref'],
  121.                     $row['site_name'],
  122.                     $row['debitor_name'],
  123.                     $row['debitor_email'],
  124.                     $row['debitor_iban'],
  125.                     $row['amount'],
  126.                     $row['currency'],
  127.                     $row['interval_value'] && $row['interval_unit'] ? 'Every ' . $row['interval_value'] . ' ' . strtolower((string) $row['interval_unit']) : '',
  128.                     $row['status'],
  129.                     $row['payments_count'],
  130.                     $total ?? 'Unlimited',
  131.                     $row['last_payment_at'] ? date('Y-m-d H:i:s', (int) $row['last_payment_at']) : '',
  132.                     $row['created_at'] ? date('Y-m-d H:i:s', (int) $row['created_at']) : '',
  133.                 ]);
  134.             }
  135.             fclose($handle);
  136.         });
  137.         $filename = 'recurring_payments_' . date('Ymd_His') . '.csv';
  138.         $response->headers->set('Content-Type', 'text/csv');
  139.         $response->headers->set('Content-Disposition', 'attachment; filename="' . $filename . '"');
  140.         return $response;
  141.     }
  142.     #[Route(path: '/{mandateId}/payments', name: 'admin_recurring_payments_child', methods: ['GET'])]
  143.     public function payments(string $mandateId, ManagerRegistry $doctrine): JsonResponse
  144.     {
  145.         $conn = $doctrine->getConnection();
  146.         $rows = $conn->fetchAllAssociative(
  147.             'SELECT id, amount, currency, type, status, reference_id, retry_count, created_at
  148.              FROM recurring_payments
  149.              WHERE mandate_id = :mandateId
  150.              ORDER BY created_at::bigint DESC',
  151.             ['mandateId' => $mandateId]
  152.         );
  153.         return new JsonResponse(['data' => $rows]);
  154.     }
  155.     #[Route(path: '/{mandateId}/stop', name: 'admin_recurring_payments_stop', methods: ['POST'])]
  156.     public function stop(string $mandateId, ManagerRegistry $doctrine): JsonResponse
  157.     {
  158.         $conn = $doctrine->getConnection();
  159.         $current = $conn->fetchOne('SELECT status FROM recurring_mandates WHERE id::text = :id', ['id' => $mandateId]);
  160.         if ($current === false) {
  161.             return $this->createErrorJsonResponse('Mandate not found.');
  162.         }
  163.         if ($current === RecurringMandateStatus::STOPPED) {
  164.             return new JsonResponse(['success' => true, 'status' => RecurringMandateStatus::STOPPED]);
  165.         }
  166.         $this->updateMandateStatus($conn, $mandateId, RecurringMandateStatus::STOPPED);
  167.         return new JsonResponse(['success' => true, 'status' => RecurringMandateStatus::STOPPED]);
  168.     }
  169.     private function updateMandateStatus(Connection $conn, string $mandateId, string $status): void
  170.     {
  171.         $conn->executeStatement(
  172.             'UPDATE recurring_mandates
  173.              SET status = :status, updated_at = EXTRACT(epoch FROM now())::bigint::text
  174.              WHERE id::text = :id',
  175.             ['status' => $status, 'id' => $mandateId]
  176.         );
  177.     }
  178. }