src/CompanyGroupBundle/Controller/QuoteController.php line 94

Open in your IDE?
  1. <?php
  2. namespace CompanyGroupBundle\Controller;
  3. use ApplicationBundle\Controller\GenericController;
  4. use ApplicationBundle\Modules\Authentication\Constants\UserConstants;
  5. use CompanyGroupBundle\Entity\SubscriptionQuote;
  6. use CompanyGroupBundle\Modules\Api\Service\LegacySubscriptionBillingService;
  7. use CompanyGroupBundle\Modules\Api\Service\PricingService;
  8. use Symfony\Component\HttpFoundation\JsonResponse;
  9. use Symfony\Component\HttpFoundation\Request;
  10. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  11. /**
  12.  * Customer-facing quote controller.
  13.  * All views extend central_header.html.twig.
  14.  * Entity manager: company_group
  15.  */
  16. class QuoteController extends GenericController
  17. {
  18.     /**
  19.      * GET /quote/promo-lookup?code=XXX
  20.      */
  21.     public function PromoCodeLookupAction(Request $request)
  22.     {
  23.         $code trim($request->query->get('code'''));
  24.         if (!$code) {
  25.             return new JsonResponse(['success' => false'message' => 'No code provided']);
  26.         }
  27.         $em $this->getDoctrine()->getManager('company_group');
  28.         $promo $em->getRepository('CompanyGroupBundle\Entity\PromoCode')->findOneBy(['code' => $code]);
  29.         if (!$promo) {
  30.             return new JsonResponse(['success' => false'message' => 'Invalid promo code']);
  31.         }
  32.         $now time();
  33.         if ($promo->getExpiresAtTs() && $promo->getExpiresAtTs() < $now) {
  34.             return new JsonResponse(['success' => false'message' => 'This promo code has expired']);
  35.         }
  36.         if ($promo->getStartsAtTs() && $promo->getStartsAtTs() > $now) {
  37.             return new JsonResponse(['success' => false'message' => 'This promo code is not yet active']);
  38.         }
  39.         if ($promo->getMaxUseCount() && $promo->getUseCountBalance() !== null && $promo->getUseCountBalance() <= 0) {
  40.             return new JsonResponse(['success' => false'message' => 'This promo code has reached its usage limit']);
  41.         }
  42.         return new JsonResponse([
  43.             'success' => true,
  44.             'id' => $promo->getId(),
  45.             'code' => $promo->getCode(),
  46.             'promo_type' => (int)$promo->getPromoType(),
  47.             'promo_value' => (float)$promo->getPromoValue(),
  48.             'max_discount' => $promo->getMaxDiscountAmount() ? (float)$promo->getMaxDiscountAmount() : null,
  49.             'min_amount' => $promo->getMinAmountForApplication() ? (float)$promo->getMinAmountForApplication() : null,
  50.             'label' => (int)$promo->getPromoType() === 1
  51.                 'EUR ' number_format((float)$promo->getPromoValue(), 2) . ' off'
  52.                 : (float)$promo->getPromoValue() . '% off',
  53.         ]);
  54.     }
  55.     /**
  56.      * POST /quote/calculate-price
  57.      */
  58.     public function CalculatePriceAction(Request $request)
  59.     {
  60.         /** @var PricingService $pricing */
  61.         $pricing $this->get('app.pricing_service');
  62.         $normal max(0, (int)$request->request->get('normal_users'0));
  63.         $admin max(0, (int)$request->request->get('admin_users'0));
  64.         $ml max(0, (int)$request->request->get('ml_users'0));
  65.         $cycle in_array($request->request->get('billing_cycle'), ['monthly''yearly'], true)
  66.             ? $request->request->get('billing_cycle')
  67.             : 'monthly';
  68.         $planType in_array($request->request->get('plan_type'), ['team''enterprise'], true)
  69.             ? $request->request->get('plan_type')
  70.             : 'team';
  71.         return new JsonResponse($pricing->getPriceBreakdown($normal$admin$ml$cycle$planType));
  72.     }
  73.     /**
  74.      * GET /quote/request
  75.      * POST /quote/request
  76.      */
  77.     public function RequestQuoteAction(Request $request)
  78.     {
  79.         $session $request->getSession();
  80.         if ($request->isMethod('GET')) {
  81.             $prefill = [
  82.                 'company_name' => $session->get('company_name'''),
  83.                 'customer_email' => $session->get(UserConstants::USER_EMAIL''),
  84.                 'customer_name' => $session->get('userName'''),
  85.                 'customer_phone' => '',
  86.                 'plan_type' => $request->query->get('plan'SubscriptionQuote::PLAN_TEAM),
  87.                 'normal_users' => max(1, (int)$request->query->get('normal_users'1)),
  88.                 'admin_users' => max(0, (int)$request->query->get('admin_users'0)),
  89.                 'ml_users' => max(0, (int)$request->query->get('ml_users'0)),
  90.                 'billing_cycle' => $request->query->get('billing_cycle''monthly'),
  91.                 'payment_type' => $request->query->get('payment_type''automatic'),
  92.             ];
  93.             $pricing $this->get('app.pricing_service');
  94.             $breakdown $pricing->getPriceBreakdown(
  95.                 $prefill['normal_users'],
  96.                 $prefill['admin_users'],
  97.                 $prefill['ml_users'],
  98.                 $prefill['billing_cycle'],
  99.                 $prefill['plan_type']
  100.             );
  101.             return $this->render('@CompanyGroup/pages/quotes/request_quote.html.twig', [
  102.                 'prefill' => $prefill,
  103.                 'breakdown' => $breakdown,
  104.                 'page_title' => 'Request a Quote',
  105.             ]);
  106.         }
  107.         $post $request->request;
  108.         $service $this->get('app.quote_service');
  109.         $data = [
  110.             'plan_type' => $post->get('plan_type'SubscriptionQuote::PLAN_TEAM),
  111.             'normal_user_count' => max(0, (int)$post->get('normal_users'0)),
  112.             'admin_user_count' => max(0, (int)$post->get('admin_users'0)),
  113.             'ml_user_count' => max(0, (int)$post->get('ml_users'0)),
  114.             'billing_cycle' => $post->get('billing_cycle''monthly'),
  115.             'payment_type' => $post->get('payment_type''automatic'),
  116.             'customer_email' => trim($post->get('customer_email''')),
  117.             'customer_name' => trim($post->get('customer_name''')),
  118.             'customer_phone' => trim($post->get('customer_phone''')),
  119.             'company_name' => trim($post->get('company_name''')),
  120.             'customer_notes' => trim($post->get('customer_notes''')),
  121.             'company_address' => trim($post->get('company_address''')),
  122.             'country' => trim($post->get('country''')),
  123.             'app_id' => $session->get('appId'),
  124.             'promo_code_id' => $post->get('promo_code_id'null),
  125.         ];
  126.         if (empty($data['customer_email'])) {
  127.             $this->addFlash('error''Please provide your email address.');
  128.             return $this->redirectToRoute('quote_request');
  129.         }
  130.         $quote $service->createCustomerQuote($data);
  131.         return $this->redirectToRoute('quote_view_customer', ['token' => $quote->getQuoteToken()]);
  132.     }
  133.     /**
  134.      * Direct onboarding ("buy now") — standard pricing, no proposal step. Creates a
  135.      * customer quote, immediately raises + accepts its invoice, and (for automatic
  136.      * payment) drops the buyer straight on the gateway. Same endpoint serves a
  137.      * self-serve signup form OR a sales rep onboarding a company on its behalf.
  138.      *
  139.      * POST /quote/onboard
  140.      */
  141.     public function DirectOnboardAction(Request $request)
  142.     {
  143.         $session $request->getSession();
  144.         $post $request->request;
  145.         $service $this->get('app.quote_service');
  146.         $data = [
  147.             'plan_type'         => $post->get('plan_type'SubscriptionQuote::PLAN_TEAM),
  148.             'normal_user_count' => max(0, (int)$post->get('normal_users'1)),
  149.             'admin_user_count'  => max(0, (int)$post->get('admin_users'0)),
  150.             'ml_user_count'     => max(0, (int)$post->get('ml_users'0)),
  151.             'billing_cycle'     => $post->get('billing_cycle''monthly'),
  152.             'payment_type'      => $post->get('payment_type''automatic'),
  153.             'customer_email'    => trim($post->get('customer_email''')),
  154.             'customer_name'     => trim($post->get('customer_name''')),
  155.             'customer_phone'    => trim($post->get('customer_phone''')),
  156.             'company_name'      => trim($post->get('company_name''')),
  157.             'customer_notes'    => trim($post->get('customer_notes''')),
  158.             'company_address'   => trim($post->get('company_address''')),
  159.             'country'           => trim($post->get('country''')),
  160.             'app_id'            => $session->get('appId'),
  161.             'promo_code_id'     => $post->get('promo_code_id'null),
  162.         ];
  163.         if (empty($data['customer_email']) || empty($data['company_name'])) {
  164.             $this->addFlash('error''Please provide a company name and email to continue.');
  165.             return $this->redirectToRoute('quote_request');
  166.         }
  167.         $quote $service->createCustomerQuote($data);
  168.         /** @var LegacySubscriptionBillingService $billing */
  169.         $billing $this->get('app.legacy_subscription_billing_service');
  170.         try {
  171.             $invoice $billing->createOrReuseQuoteInvoice($quote, (int)$this->loggedUserId($request));
  172.             $service->customerAccept($quote); // no status guard — direct accept is allowed
  173.         } catch (\RuntimeException $e) {
  174.             $this->addFlash('error'$e->getMessage() . ' Please contact support to finish setup.');
  175.             return $this->redirectToRoute('quote_view_customer', ['token' => $quote->getQuoteToken()]);
  176.         }
  177.         if ($quote->getPaymentType() === SubscriptionQuote::PAYMENT_AUTOMATIC) {
  178.             return $this->redirectToRoute('quote_payment_redirect', [
  179.                 'token' => $quote->getQuoteToken(),
  180.                 'invoice_id' => $invoice->getId(),
  181.             ]);
  182.         }
  183.         $this->addFlash('success''Account set up. Your first invoice is ready for payment.');
  184.         return $this->redirectToRoute('quote_view_customer', ['token' => $quote->getQuoteToken()]);
  185.     }
  186.     /**
  187.      * GET /quote/{token}
  188.      */
  189.     public function ViewQuoteAction(Request $request$token)
  190.     {
  191.         $service $this->get('app.quote_service');
  192.         $quote $service->findByToken($token);
  193.         if (!$quote) {
  194.             throw $this->createNotFoundException('Quote not found.');
  195.         }
  196.         $history $service->getHistory((int)$quote->getId());
  197.         $pricing $this->get('app.pricing_service');
  198.         $breakdown null;
  199.         if ($quote->getNormalUserCount() !== null) {
  200.             $breakdown $pricing->getPriceBreakdown(
  201.                 (int)$quote->getNormalUserCount(),
  202.                 (int)$quote->getAdminUserCount(),
  203.                 (int)$quote->getMlUserCount(),
  204.                 $quote->getBillingCycle() ?: 'monthly',
  205.                 $quote->getPlanType()
  206.             );
  207.         }
  208.         return $this->render('@CompanyGroup/pages/quotes/customer_quote_view.html.twig', [
  209.             'quote' => $quote,
  210.             'history' => $history,
  211.             'breakdown' => $breakdown,
  212.             'page_title' => 'Your Quote - HoneyBee ERP',
  213.         ]);
  214.     }
  215.     /**
  216.      * GET /quote/{token}/print
  217.      */
  218.     public function PrintQuoteAction(Request $request$token)
  219.     {
  220.         $service $this->get('app.quote_service');
  221.         $quote $service->findByToken($token);
  222.         if (!$quote) {
  223.             throw $this->createNotFoundException('Quote not found.');
  224.         }
  225.         $pricing $this->get('app.pricing_service');
  226.         $breakdown null;
  227.         if ($quote->getNormalUserCount() !== null) {
  228.             $breakdown $pricing->getPriceBreakdown(
  229.                 (int)$quote->getNormalUserCount(),
  230.                 (int)$quote->getAdminUserCount(),
  231.                 (int)$quote->getMlUserCount(),
  232.                 $quote->getBillingCycle() ?: 'monthly',
  233.                 $quote->getPlanType()
  234.             );
  235.         }
  236.         return $this->render('@CompanyGroup/pages/quotes/quote_print.html.twig', [
  237.             'quote' => $quote,
  238.             'breakdown' => $breakdown,
  239.         ]);
  240.     }
  241.     /**
  242.      * POST /quote/{token}/accept
  243.      */
  244.     public function AcceptQuoteAction(Request $request$token)
  245.     {
  246.         $service $this->get('app.quote_service');
  247.         $quote $service->findByToken($token);
  248.         if (!$quote) {
  249.             throw $this->createNotFoundException('Quote not found.');
  250.         }
  251.         $allowedStatuses = [
  252.             SubscriptionQuote::STATUS_SENT,
  253.             SubscriptionQuote::STATUS_MODIFIED,
  254.         ];
  255.         if (!in_array($quote->getStatus(), $allowedStatusestrue)) {
  256.             $this->addFlash('error''This quote cannot be accepted in its current state.');
  257.             return $this->redirectToRoute('quote_view_customer', ['token' => $token]);
  258.         }
  259.         /** @var LegacySubscriptionBillingService $billing */
  260.         $billing $this->get('app.legacy_subscription_billing_service');
  261.         try {
  262.             $invoice $billing->createOrReuseQuoteInvoice($quote, (int)$this->loggedUserId($request));
  263.             $service->customerAccept($quote);
  264.         } catch (\RuntimeException $e) {
  265.             $this->addFlash('error'$e->getMessage() . ' Please contact support to finish setup before payment.');
  266.             return $this->redirectToRoute('quote_view_customer', ['token' => $token]);
  267.         }
  268.         if ($quote->getPaymentType() === SubscriptionQuote::PAYMENT_AUTOMATIC) {
  269.             $this->addFlash('success''Quote accepted. Redirecting you to secure payment.');
  270.             return $this->redirectToRoute('quote_payment_redirect', [
  271.                 'token' => $token,
  272.                 'invoice_id' => $invoice->getId(),
  273.             ]);
  274.         }
  275.         $this->addFlash('success''Quote accepted. Your invoice is waiting for payment confirmation.');
  276.         return $this->redirectToRoute('quote_view_customer', ['token' => $token]);
  277.     }
  278.     /**
  279.      * POST /quote/{token}/reject
  280.      */
  281.     public function RejectQuoteAction(Request $request$token)
  282.     {
  283.         $service $this->get('app.quote_service');
  284.         $quote $service->findByToken($token);
  285.         if (!$quote) {
  286.             throw $this->createNotFoundException('Quote not found.');
  287.         }
  288.         $reason trim($request->request->get('reason'''));
  289.         $service->customerReject($quote$reason ?: null);
  290.         $this->addFlash('info''Quote rejected. Our team will be in touch if you would like to discuss further.');
  291.         return $this->redirectToRoute('quote_view_customer', ['token' => $token]);
  292.     }
  293.     /**
  294.      * GET /quote/{token}/pay/{invoice_id}
  295.      */
  296.     public function PaymentRedirectAction(Request $request$token$invoice_id)
  297.     {
  298.         $service $this->get('app.quote_service');
  299.         $quote $service->findByToken($token);
  300.         if (!$quote) {
  301.             throw $this->createNotFoundException('Quote not found.');
  302.         }
  303.         /** @var LegacySubscriptionBillingService $billing */
  304.         $billing $this->get('app.legacy_subscription_billing_service');
  305.         $invoice = (int)$invoice_id 0
  306.             $this->getDoctrine()->getManager('company_group')
  307.                 ->getRepository('CompanyGroupBundle\Entity\EntityInvoice')
  308.                 ->find((int)$invoice_id)
  309.             : $billing->findQuoteInvoice($quote);
  310.         if (!$invoice) {
  311.             throw $this->createNotFoundException('Invoice not found.');
  312.         }
  313.         $successActionData json_decode((string)$invoice->getSuccessActionData(), true);
  314.         $linkedQuoteId is_array($successActionData) ? (int)($successActionData['quoteId'] ?? 0) : 0;
  315.         if ($linkedQuoteId !== (int)$quote->getId()) {
  316.             throw $this->createAccessDeniedException('Invoice does not belong to this quote.');
  317.         }
  318.         if ($quote->getPaymentType() !== SubscriptionQuote::PAYMENT_AUTOMATIC) {
  319.             $this->addFlash('info''This quote is configured for manual payment confirmation.');
  320.             return $this->redirectToRoute('quote_view_customer', ['token' => $token]);
  321.         }
  322.         $encData $this->get('url_encryptor')->encrypt(json_encode(
  323.             $billing->buildPaymentRedirectPayload($invoice11)
  324.         ));
  325.         return $this->redirect($this->generateUrl('make_payment_of_entity_invoice', [
  326.             'encData' => $encData,
  327.         ], UrlGeneratorInterface::ABSOLUTE_URL));
  328.     }
  329. }