src/CompanyGroupBundle/Controller/AdminInvoiceController.php line 141

Open in your IDE?
  1. <?php
  2. namespace CompanyGroupBundle\Controller;
  3. use ApplicationBundle\Constants\BuddybeeConstant;
  4. use ApplicationBundle\Controller\GenericController;
  5. use ApplicationBundle\Modules\Authentication\Constants\UserConstants;
  6. use ApplicationBundle\Modules\Buddybee\Buddybee;
  7. use CompanyGroupBundle\Entity\CompanyGroup;
  8. use CompanyGroupBundle\Entity\EntityApplicantDetails;
  9. use CompanyGroupBundle\Entity\EntityInvoice;
  10. use CompanyGroupBundle\Entity\SubscriptionQuote;
  11. use Doctrine\ORM\EntityManagerInterface;
  12. use Symfony\Component\HttpFoundation\JsonResponse;
  13. use Symfony\Component\HttpFoundation\Request;
  14. /**
  15.  * Admin invoice management.
  16.  * Entity manager: company_group
  17.  */
  18. class AdminInvoiceController extends GenericController
  19. {
  20.     const INVOICES_PER_PAGE 20;
  21.     private function requireAdminAccess(Request $request): bool
  22.     {
  23.         $session    $request->getSession();
  24.         $userId     = (int)$session->get(UserConstants::USER_ID0);
  25.         $isBuddybee = (int)$session->get(UserConstants::IS_BUDDYBEE_ADMIN0);
  26.         $allAccess  = (int)$session->get(UserConstants::ALL_MODULE_ACCESS_FLAG0);
  27.         return $userId && ($isBuddybee === || $allAccess === 1);
  28.     }
  29.     private function invoiceStatus(EntityInvoice $invoice): string
  30.     {
  31.         if ((int)$invoice->getStage() === BuddybeeConstant::ENTITY_INVOICE_STAGE_COMPLETED || (int)$invoice->getIsProcessed() === 1) {
  32.             return 'paid';
  33.         }
  34.         if ((int)$invoice->getStage() === BuddybeeConstant::ENTITY_INVOICE_STAGE_FAILED) {
  35.             return 'failed';
  36.         }
  37.         if ((int)$invoice->getStage() === BuddybeeConstant::ENTITY_INVOICE_STAGE_CANCELLED) {
  38.             return 'cancelled';
  39.         }
  40.         return 'pending';
  41.     }
  42.     private function normalizeLegacyInvoice(EntityManagerInterface $emEntityInvoice $invoice): array
  43.     {
  44.         $successActionData json_decode((string)$invoice->getSuccessActionData(), true);
  45.         if (!is_array($successActionData)) {
  46.             $successActionData = [];
  47.         }
  48.         $quote null;
  49.         $quoteId = (int)($successActionData['quoteId'] ?? 0);
  50.         if ($quoteId 0) {
  51.             $quote $em->getRepository(SubscriptionQuote::class)->find($quoteId);
  52.         }
  53.         $appId = (int)$invoice->getAppId();
  54.         $company $appId $em->getRepository(CompanyGroup::class)->findOneBy(['appId' => $appId]) : null;
  55.         $billTo $invoice->getBillToId() ? $em->getRepository(EntityApplicantDetails::class)->find((int)$invoice->getBillToId()) : null;
  56.         $companyName '';
  57.         if ($company && method_exists($company'getCompanyName')) {
  58.             $companyName = (string)$company->getCompanyName();
  59.         }
  60.         if ($companyName === '' && $quote) {
  61.             $companyName = (string)$quote->getCompanyName();
  62.         }
  63.         if ($companyName === '' && isset($successActionData['pendingCompany']['companyName'])) {
  64.             $companyName = (string)$successActionData['pendingCompany']['companyName'];
  65.         }
  66.         $customerName $quote ? (string)$quote->getCustomerName() : '';
  67.         if ($customerName === '' && $billTo) {
  68.             $customerName trim((string)$billTo->getFirstname() . ' ' . (string)$billTo->getLastName());
  69.         }
  70.         if ($customerName === '' && isset($successActionData['pendingCompany']['customerName'])) {
  71.             $customerName = (string)$successActionData['pendingCompany']['customerName'];
  72.         }
  73.         $customerEmail $quote ? (string)$quote->getCustomerEmail() : '';
  74.         if ($customerEmail === '' && isset($successActionData['pendingCompany']['customerEmail'])) {
  75.             $customerEmail = (string)$successActionData['pendingCompany']['customerEmail'];
  76.         }
  77.         if ($customerEmail === '' && $billTo) {
  78.             $customerEmail = (string)($billTo->getOAuthEmail() ?: $billTo->getEmail());
  79.         }
  80.         $metaQuote = [
  81.             'id' => $quote ? (int)$quote->getId() : 0,
  82.             'createdAt' => $quote $quote->getCreatedAt() : null,
  83.             'status' => $quote ? (string)$quote->getStatus() : '',
  84.         ];
  85.         $invoiceDate $invoice->getInvoiceDate();
  86.         $expiryTs = (int)$invoice->getExpireIfUnpaidTs();
  87.         $dueAt $expiryTs ? (new \DateTime())->setTimestamp($expiryTs) : null;
  88.         $status $this->invoiceStatus($invoice);
  89.         $manualReference = isset($successActionData['manualTransactionReference']) ? (string)$successActionData['manualTransactionReference'] : '';
  90.         return [
  91.             'id' => (int)$invoice->getId(),
  92.             'invoiceNumber' => (string)($invoice->getDocumentHash() ?: ('EI-' str_pad((string)$invoice->getId(), 8'0'STR_PAD_LEFT))),
  93.             'companyName' => $companyName,
  94.             'customerName' => $customerName,
  95.             'customerEmail' => $customerEmail,
  96.             'planType' => (string)($successActionData['planType'] ?? ($quote $quote->getPlanType() : '')),
  97.             'billingCycle' => (string)($successActionData['billingCycle'] ?? ($quote $quote->getBillingCycle() : 'monthly')),
  98.             'paymentType' => $invoice->getAmountTransferGateWayHash() === 'stripe' 'automatic' 'manual',
  99.             'finalAmount' => (float)$invoice->getAmount(),
  100.             'amount' => (float)$invoice->getAmount(),
  101.             'discountAmount' => (float)$invoice->getPromoDiscountAmount(),
  102.             'status' => $status,
  103.             'createdAt' => $invoiceDate,
  104.             'dueAt' => $dueAt,
  105.             'paidAt' => $status === 'paid' $invoiceDate null,
  106.             'gatewayTransactionId' => $manualReference,
  107.             'normalUserCount' => (int)($successActionData['userModification'] ?? 0),
  108.             'adminUserCount' => (int)($successActionData['adminModification'] ?? 0),
  109.             'mlUserCount' => (int)($successActionData['mlUserCount'] ?? 0),
  110.             'notes' => isset($successActionData['reason']) ? ('Reason: ' . (string)$successActionData['reason']) : '',
  111.             'quote' => $metaQuote,
  112.             'legacyInvoice' => $invoice,
  113.         ];
  114.     }
  115.     // =========================================================================
  116.     // LIST INVOICES
  117.     // =========================================================================
  118.     /**
  119.      * GET /admin/invoices
  120.      */
  121.     public function ListInvoicesAction(Request $request)
  122.     {
  123.         if (!$this->requireAdminAccess($request)) {
  124.             return $this->redirectToRoute('dashboard');
  125.         }
  126.         $em      $this->getDoctrine()->getManager('company_group');
  127.         $page    max(1, (int)$request->query->get('page'1));
  128.         $offset  = ($page 1) * self::INVOICES_PER_PAGE;
  129.         $filters = [
  130.             'status'       => $request->query->get('status',       ''),
  131.             'payment_type' => $request->query->get('payment_type'''),
  132.             'search'       => trim($request->query->get('q',       '')),
  133.         ];
  134.         $qb $em->getRepository(EntityInvoice::class)->createQueryBuilder('i')
  135.             ->where('i.invoiceType = :invoiceType')
  136.             ->setParameter('invoiceType'BuddybeeConstant::ENTITY_INVOICE_TYPE_PAYMENT_TO_HONEYBEE)
  137.             ->orderBy('i.Id''DESC');
  138.         if ($filters['payment_type'] === 'automatic') {
  139.             $qb->andWhere('i.amountTransferGateWayHash = :gatewayAutomatic')
  140.                 ->setParameter('gatewayAutomatic''stripe');
  141.         } elseif ($filters['payment_type'] === 'manual') {
  142.             $qb->andWhere('i.amountTransferGateWayHash != :gatewayAutomatic')
  143.                 ->setParameter('gatewayAutomatic''stripe');
  144.         }
  145.         if ($filters['status'] === 'paid') {
  146.             $qb->andWhere('i.stage = :stagePaid OR i.isProcessed = 1')
  147.                 ->setParameter('stagePaid'BuddybeeConstant::ENTITY_INVOICE_STAGE_COMPLETED);
  148.         } elseif ($filters['status'] === 'pending') {
  149.             $qb->andWhere('i.stage = :stagePending AND i.isProcessed = 0')
  150.                 ->setParameter('stagePending'BuddybeeConstant::ENTITY_INVOICE_STAGE_INITIATED);
  151.         } elseif ($filters['status'] === 'failed') {
  152.             $qb->andWhere('i.stage = :stageFailed')
  153.                 ->setParameter('stageFailed'BuddybeeConstant::ENTITY_INVOICE_STAGE_FAILED);
  154.         } elseif ($filters['status'] === 'cancelled') {
  155.             $qb->andWhere('i.stage = :stageCancelled')
  156.                 ->setParameter('stageCancelled'BuddybeeConstant::ENTITY_INVOICE_STAGE_CANCELLED);
  157.         }
  158.         if ($filters['search'] !== '') {
  159.             $qb->andWhere('i.documentHash LIKE :search OR i.successActionData LIKE :search')
  160.                 ->setParameter('search''%' $filters['search'] . '%');
  161.         }
  162.         $allInvoices $qb->getQuery()->getResult();
  163.         $normalized array_map(function (EntityInvoice $invoice) use ($em) {
  164.             return $this->normalizeLegacyInvoice($em$invoice);
  165.         }, $allInvoices);
  166.         if ($filters['search'] !== '') {
  167.             $needle mb_strtolower($filters['search']);
  168.             $normalized array_values(array_filter($normalized, function (array $invoice) use ($needle) {
  169.                 $haystacks = [
  170.                     (string)$invoice['invoiceNumber'],
  171.                     (string)$invoice['companyName'],
  172.                     (string)$invoice['customerName'],
  173.                     (string)$invoice['customerEmail'],
  174.                 ];
  175.                 foreach ($haystacks as $haystack) {
  176.                     if ($haystack !== '' && mb_strpos(mb_strtolower($haystack), $needle) !== false) {
  177.                         return true;
  178.                     }
  179.                 }
  180.                 return false;
  181.             }));
  182.         }
  183.         $total count($normalized);
  184.         $items array_slice($normalized$offsetself::INVOICES_PER_PAGE);
  185.         $totalPages = (int)ceil($total self::INVOICES_PER_PAGE);
  186.         $stats = [
  187.             'total' => count(array_filter($normalized, function () { return true; })),
  188.             'pending' => count(array_filter($normalized, function (array $invoice) { return $invoice['status'] === 'pending'; })),
  189.             'paid' => count(array_filter($normalized, function (array $invoice) { return $invoice['status'] === 'paid'; })),
  190.             'failed' => count(array_filter($normalized, function (array $invoice) { return $invoice['status'] === 'failed'; })),
  191.             'revenue' => array_reduce($normalized, function ($carry, array $invoice) {
  192.                 return $invoice['status'] === 'paid' $carry + (float)$invoice['finalAmount'] : $carry;
  193.             }, 0.0),
  194.         ];
  195.         return $this->render('@CompanyGroup/pages/admin/invoices/list_invoices.html.twig', [
  196.             'page_title'  => 'Invoices',
  197.             'invoices'    => $items,
  198.             'total'       => $total,
  199.             'currentPage' => $page,
  200.             'totalPages'  => $totalPages,
  201.             'filters'     => $filters,
  202.             'stats'       => $stats,
  203.         ]);
  204.     }
  205.     // =========================================================================
  206.     // VIEW INVOICE
  207.     // =========================================================================
  208.     /**
  209.      * GET /admin/invoices/{id}
  210.      */
  211.     public function ViewInvoiceAction(Request $request$id)
  212.     {
  213.         if (!$this->requireAdminAccess($request)) {
  214.             return $this->redirectToRoute('dashboard');
  215.         }
  216.         $em $this->getDoctrine()->getManager('company_group');
  217.         $legacyInvoice $em->getRepository(EntityInvoice::class)->find((int)$id);
  218.         if (!$legacyInvoice || (int)$legacyInvoice->getInvoiceType() !== BuddybeeConstant::ENTITY_INVOICE_TYPE_PAYMENT_TO_HONEYBEE) {
  219.             throw $this->createNotFoundException('Invoice #' $id ' not found.');
  220.         }
  221.         $invoice $this->normalizeLegacyInvoice($em$legacyInvoice);
  222.         $quote = !empty($invoice['quote']['id'])
  223.             ? $em->getRepository(SubscriptionQuote::class)->find((int)$invoice['quote']['id'])
  224.             : null;
  225.         return $this->render('@CompanyGroup/pages/admin/invoices/view_invoice.html.twig', [
  226.             'page_title' => 'Invoice ' $invoice['invoiceNumber'],
  227.             'invoice'    => $invoice,
  228.             'quote'      => $quote,
  229.         ]);
  230.     }
  231.     // =========================================================================
  232.     // MARK PAID (manual payment)
  233.     // =========================================================================
  234.     /**
  235.      * POST /admin/invoices/{id}/mark-paid
  236.      */
  237.     public function MarkPaidAction(Request $request$id)
  238.     {
  239.         if (!$this->requireAdminAccess($request)) {
  240.             return new JsonResponse(['success' => false'message' => 'Unauthorized'], 403);
  241.         }
  242.         $em $this->getDoctrine()->getManager('company_group');
  243.         $invoice $em->getRepository(EntityInvoice::class)->find((int)$id);
  244.         if (!$invoice || (int)$invoice->getInvoiceType() !== BuddybeeConstant::ENTITY_INVOICE_TYPE_PAYMENT_TO_HONEYBEE) {
  245.             return new JsonResponse(['success' => false'message' => 'Invoice not found'], 404);
  246.         }
  247.         $txRef trim($request->request->get('transaction_reference'''));
  248.         if ($txRef !== '') {
  249.             $successActionData json_decode((string)$invoice->getSuccessActionData(), true);
  250.             if (!is_array($successActionData)) {
  251.                 $successActionData = [];
  252.             }
  253.             $successActionData['manualTransactionReference'] = $txRef;
  254.             $invoice->setSuccessActionData(json_encode($successActionData));
  255.             $em->flush();
  256.         }
  257.         $this->get('app.quote_company_provisioning_service')
  258.             ->ensureCompanyForInvoice($invoice$request->getSession(), $invoice->getStripeCustomerId() ?: null);
  259.         Buddybee::ProcessEntityInvoice(
  260.             $em,
  261.             (int)$invoice->getId(),
  262.             ['stage' => BuddybeeConstant::ENTITY_INVOICE_STAGE_COMPLETED],
  263.             $this->container->getParameter('kernel.root_dir'),
  264.             false,
  265.             $this->container->getParameter('notification_enabled'),
  266.             $this->container->getParameter('notification_server')
  267.         );
  268.         $this->get('app.subscription_state_sync_service')->syncFromLegacyInvoice($invoice);
  269.         $successActionData json_decode((string)$invoice->getSuccessActionData(), true);
  270.         if (!is_array($successActionData)) {
  271.             $successActionData = [];
  272.         }
  273.         $ownerId = (int)($successActionData['ownerId'] ?? $invoice->getApplicantId());
  274.         $appId = (int)($invoice->getAppId() ?: ($successActionData['appId'] ?? 0));
  275.         if ($ownerId && $appId 0) {
  276.             $this->get('app.post_payment_company_setup_service')
  277.                 ->finalizeOwnerServerSync($ownerId);
  278.         }
  279.         $displayNumber $invoice->getDocumentHash() ?: ('EI-' str_pad((string)$invoice->getId(), 8'0'STR_PAD_LEFT));
  280.         $this->addFlash('success''Invoice ' $displayNumber ' marked as paid.');
  281.         return $this->redirectToRoute('admin_invoice_view', ['id' => $id]);
  282.     }
  283.     // =========================================================================
  284.     // MARK FAILED
  285.     // =========================================================================
  286.     /**
  287.      * POST /admin/invoices/{id}/mark-failed
  288.      */
  289.     public function MarkFailedAction(Request $request$id)
  290.     {
  291.         if (!$this->requireAdminAccess($request)) {
  292.             return new JsonResponse(['success' => false'message' => 'Unauthorized'], 403);
  293.         }
  294.         $em $this->getDoctrine()->getManager('company_group');
  295.         $invoice $em->getRepository(EntityInvoice::class)->find((int)$id);
  296.         if (!$invoice || (int)$invoice->getInvoiceType() !== BuddybeeConstant::ENTITY_INVOICE_TYPE_PAYMENT_TO_HONEYBEE) {
  297.             return new JsonResponse(['success' => false'message' => 'Invoice not found'], 404);
  298.         }
  299.         $invoice->setStage(BuddybeeConstant::ENTITY_INVOICE_STAGE_FAILED);
  300.         $invoice->setIsProcessed(0);
  301.         $em->flush();
  302.         $displayNumber $invoice->getDocumentHash() ?: ('EI-' str_pad((string)$invoice->getId(), 8'0'STR_PAD_LEFT));
  303.         $this->addFlash('info''Invoice ' $displayNumber ' marked as failed.');
  304.         return $this->redirectToRoute('admin_invoice_view', ['id' => $id]);
  305.     }
  306. }