<?php
namespace CompanyGroupBundle\Controller;
use ApplicationBundle\Controller\GenericController;
use ApplicationBundle\Modules\Authentication\Constants\UserConstants;
use CompanyGroupBundle\Entity\SubscriptionQuote;
use CompanyGroupBundle\Modules\Api\Service\LegacySubscriptionBillingService;
use CompanyGroupBundle\Modules\Api\Service\PricingService;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
/**
* Customer-facing quote controller.
* All views extend central_header.html.twig.
* Entity manager: company_group
*/
class QuoteController extends GenericController
{
/**
* GET /quote/promo-lookup?code=XXX
*/
public function PromoCodeLookupAction(Request $request)
{
$code = trim($request->query->get('code', ''));
if (!$code) {
return new JsonResponse(['success' => false, 'message' => 'No code provided']);
}
$em = $this->getDoctrine()->getManager('company_group');
$promo = $em->getRepository('CompanyGroupBundle\Entity\PromoCode')->findOneBy(['code' => $code]);
if (!$promo) {
return new JsonResponse(['success' => false, 'message' => 'Invalid promo code']);
}
$now = time();
if ($promo->getExpiresAtTs() && $promo->getExpiresAtTs() < $now) {
return new JsonResponse(['success' => false, 'message' => 'This promo code has expired']);
}
if ($promo->getStartsAtTs() && $promo->getStartsAtTs() > $now) {
return new JsonResponse(['success' => false, 'message' => 'This promo code is not yet active']);
}
if ($promo->getMaxUseCount() && $promo->getUseCountBalance() !== null && $promo->getUseCountBalance() <= 0) {
return new JsonResponse(['success' => false, 'message' => 'This promo code has reached its usage limit']);
}
return new JsonResponse([
'success' => true,
'id' => $promo->getId(),
'code' => $promo->getCode(),
'promo_type' => (int)$promo->getPromoType(),
'promo_value' => (float)$promo->getPromoValue(),
'max_discount' => $promo->getMaxDiscountAmount() ? (float)$promo->getMaxDiscountAmount() : null,
'min_amount' => $promo->getMinAmountForApplication() ? (float)$promo->getMinAmountForApplication() : null,
'label' => (int)$promo->getPromoType() === 1
? 'EUR ' . number_format((float)$promo->getPromoValue(), 2) . ' off'
: (float)$promo->getPromoValue() . '% off',
]);
}
/**
* POST /quote/calculate-price
*/
public function CalculatePriceAction(Request $request)
{
/** @var PricingService $pricing */
$pricing = $this->get('app.pricing_service');
$normal = max(0, (int)$request->request->get('normal_users', 0));
$admin = max(0, (int)$request->request->get('admin_users', 0));
$ml = max(0, (int)$request->request->get('ml_users', 0));
$cycle = in_array($request->request->get('billing_cycle'), ['monthly', 'yearly'], true)
? $request->request->get('billing_cycle')
: 'monthly';
$planType = in_array($request->request->get('plan_type'), ['team', 'enterprise'], true)
? $request->request->get('plan_type')
: 'team';
return new JsonResponse($pricing->getPriceBreakdown($normal, $admin, $ml, $cycle, $planType));
}
/**
* GET /quote/request
* POST /quote/request
*/
public function RequestQuoteAction(Request $request)
{
$session = $request->getSession();
if ($request->isMethod('GET')) {
$prefill = [
'company_name' => $session->get('company_name', ''),
'customer_email' => $session->get(UserConstants::USER_EMAIL, ''),
'customer_name' => $session->get('userName', ''),
'customer_phone' => '',
'plan_type' => $request->query->get('plan', SubscriptionQuote::PLAN_TEAM),
'normal_users' => max(1, (int)$request->query->get('normal_users', 1)),
'admin_users' => max(0, (int)$request->query->get('admin_users', 0)),
'ml_users' => max(0, (int)$request->query->get('ml_users', 0)),
'billing_cycle' => $request->query->get('billing_cycle', 'monthly'),
'payment_type' => $request->query->get('payment_type', 'automatic'),
];
$pricing = $this->get('app.pricing_service');
$breakdown = $pricing->getPriceBreakdown(
$prefill['normal_users'],
$prefill['admin_users'],
$prefill['ml_users'],
$prefill['billing_cycle'],
$prefill['plan_type']
);
return $this->render('@CompanyGroup/pages/quotes/request_quote.html.twig', [
'prefill' => $prefill,
'breakdown' => $breakdown,
'page_title' => 'Request a Quote',
]);
}
$post = $request->request;
$service = $this->get('app.quote_service');
$data = [
'plan_type' => $post->get('plan_type', SubscriptionQuote::PLAN_TEAM),
'normal_user_count' => max(0, (int)$post->get('normal_users', 0)),
'admin_user_count' => max(0, (int)$post->get('admin_users', 0)),
'ml_user_count' => max(0, (int)$post->get('ml_users', 0)),
'billing_cycle' => $post->get('billing_cycle', 'monthly'),
'payment_type' => $post->get('payment_type', 'automatic'),
'customer_email' => trim($post->get('customer_email', '')),
'customer_name' => trim($post->get('customer_name', '')),
'customer_phone' => trim($post->get('customer_phone', '')),
'company_name' => trim($post->get('company_name', '')),
'customer_notes' => trim($post->get('customer_notes', '')),
'company_address' => trim($post->get('company_address', '')),
'country' => trim($post->get('country', '')),
'app_id' => $session->get('appId'),
'promo_code_id' => $post->get('promo_code_id', null),
];
if (empty($data['customer_email'])) {
$this->addFlash('error', 'Please provide your email address.');
return $this->redirectToRoute('quote_request');
}
$quote = $service->createCustomerQuote($data);
return $this->redirectToRoute('quote_view_customer', ['token' => $quote->getQuoteToken()]);
}
/**
* GET /quote/{token}
*/
public function ViewQuoteAction(Request $request, $token)
{
$service = $this->get('app.quote_service');
$quote = $service->findByToken($token);
if (!$quote) {
throw $this->createNotFoundException('Quote not found.');
}
$history = $service->getHistory((int)$quote->getId());
$pricing = $this->get('app.pricing_service');
$breakdown = null;
if ($quote->getNormalUserCount() !== null) {
$breakdown = $pricing->getPriceBreakdown(
(int)$quote->getNormalUserCount(),
(int)$quote->getAdminUserCount(),
(int)$quote->getMlUserCount(),
$quote->getBillingCycle() ?: 'monthly',
$quote->getPlanType()
);
}
return $this->render('@CompanyGroup/pages/quotes/customer_quote_view.html.twig', [
'quote' => $quote,
'history' => $history,
'breakdown' => $breakdown,
'page_title' => 'Your Quote - HoneyBee ERP',
]);
}
/**
* GET /quote/{token}/print
*/
public function PrintQuoteAction(Request $request, $token)
{
$service = $this->get('app.quote_service');
$quote = $service->findByToken($token);
if (!$quote) {
throw $this->createNotFoundException('Quote not found.');
}
$pricing = $this->get('app.pricing_service');
$breakdown = null;
if ($quote->getNormalUserCount() !== null) {
$breakdown = $pricing->getPriceBreakdown(
(int)$quote->getNormalUserCount(),
(int)$quote->getAdminUserCount(),
(int)$quote->getMlUserCount(),
$quote->getBillingCycle() ?: 'monthly',
$quote->getPlanType()
);
}
return $this->render('@CompanyGroup/pages/quotes/quote_print.html.twig', [
'quote' => $quote,
'breakdown' => $breakdown,
]);
}
/**
* POST /quote/{token}/accept
*/
public function AcceptQuoteAction(Request $request, $token)
{
$service = $this->get('app.quote_service');
$quote = $service->findByToken($token);
if (!$quote) {
throw $this->createNotFoundException('Quote not found.');
}
$allowedStatuses = [
SubscriptionQuote::STATUS_SENT,
SubscriptionQuote::STATUS_MODIFIED,
];
if (!in_array($quote->getStatus(), $allowedStatuses, true)) {
$this->addFlash('error', 'This quote cannot be accepted in its current state.');
return $this->redirectToRoute('quote_view_customer', ['token' => $token]);
}
/** @var LegacySubscriptionBillingService $billing */
$billing = $this->get('app.legacy_subscription_billing_service');
try {
$invoice = $billing->createOrReuseQuoteInvoice($quote, (int)$this->loggedUserId($request));
$service->customerAccept($quote);
} catch (\RuntimeException $e) {
$this->addFlash('error', $e->getMessage() . ' Please contact support to finish setup before payment.');
return $this->redirectToRoute('quote_view_customer', ['token' => $token]);
}
if ($quote->getPaymentType() === SubscriptionQuote::PAYMENT_AUTOMATIC) {
$this->addFlash('success', 'Quote accepted. Redirecting you to secure payment.');
return $this->redirectToRoute('quote_payment_redirect', [
'token' => $token,
'invoice_id' => $invoice->getId(),
]);
}
$this->addFlash('success', 'Quote accepted. Your invoice is waiting for payment confirmation.');
return $this->redirectToRoute('quote_view_customer', ['token' => $token]);
}
/**
* POST /quote/{token}/reject
*/
public function RejectQuoteAction(Request $request, $token)
{
$service = $this->get('app.quote_service');
$quote = $service->findByToken($token);
if (!$quote) {
throw $this->createNotFoundException('Quote not found.');
}
$reason = trim($request->request->get('reason', ''));
$service->customerReject($quote, $reason ?: null);
$this->addFlash('info', 'Quote rejected. Our team will be in touch if you would like to discuss further.');
return $this->redirectToRoute('quote_view_customer', ['token' => $token]);
}
/**
* GET /quote/{token}/pay/{invoice_id}
*/
public function PaymentRedirectAction(Request $request, $token, $invoice_id)
{
$service = $this->get('app.quote_service');
$quote = $service->findByToken($token);
if (!$quote) {
throw $this->createNotFoundException('Quote not found.');
}
/** @var LegacySubscriptionBillingService $billing */
$billing = $this->get('app.legacy_subscription_billing_service');
$invoice = (int)$invoice_id > 0
? $this->getDoctrine()->getManager('company_group')
->getRepository('CompanyGroupBundle\Entity\EntityInvoice')
->find((int)$invoice_id)
: $billing->findQuoteInvoice($quote);
if (!$invoice) {
throw $this->createNotFoundException('Invoice not found.');
}
$successActionData = json_decode((string)$invoice->getSuccessActionData(), true);
$linkedQuoteId = is_array($successActionData) ? (int)($successActionData['quoteId'] ?? 0) : 0;
if ($linkedQuoteId !== (int)$quote->getId()) {
throw $this->createAccessDeniedException('Invoice does not belong to this quote.');
}
if ($quote->getPaymentType() !== SubscriptionQuote::PAYMENT_AUTOMATIC) {
$this->addFlash('info', 'This quote is configured for manual payment confirmation.');
return $this->redirectToRoute('quote_view_customer', ['token' => $token]);
}
$encData = $this->get('url_encryptor')->encrypt(json_encode(
$billing->buildPaymentRedirectPayload($invoice, 1, 1)
));
return $this->redirect($this->generateUrl('make_payment_of_entity_invoice', [
'encData' => $encData,
], UrlGeneratorInterface::ABSOLUTE_URL));
}
}