<?php
namespace CompanyGroupBundle\Controller;
use ApplicationBundle\Constants\GeneralConstant;
use ApplicationBundle\Controller\GenericController;
use ApplicationBundle\Modules\Authentication\Constants\UserConstants;
use CompanyGroupBundle\Entity\SubscriptionQuote;
use CompanyGroupBundle\Modules\Api\Service\LegacySubscriptionBillingService;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Generator\UrlGenerator;
/**
* Admin quote management.
* All views extend central_header.html.twig.
* Entity manager: company_group
*/
class AdminQuoteController extends GenericController
{
const QUOTES_PER_PAGE = 20;
// =========================================================================
// ACCESS CHECK
// =========================================================================
private function requireAdminAccess(Request $request): bool
{
$session = $request->getSession();
$userId = (int)$session->get(UserConstants::USER_ID, 0);
$isBuddybee = (int)$session->get(UserConstants::IS_BUDDYBEE_ADMIN, 0);
$allAccess = (int)$session->get(UserConstants::ALL_MODULE_ACCESS_FLAG, 0);
return $userId > 0 && ($isBuddybee === 1 || $allAccess === 1);
}
// =========================================================================
// LIST QUOTES
// =========================================================================
/**
* GET /admin/quotes
*/
public function ListQuotesAction(Request $request)
{
if (!$this->requireAdminAccess($request)) {
return $this->redirectToRoute('dashboard');
}
$service = $this->get('app.quote_service');
$page = max(1, (int)$request->query->get('page', 1));
$offset = ($page - 1) * self::QUOTES_PER_PAGE;
$filters = [
'status' => $request->query->get('status', ''),
'plan_type' => $request->query->get('plan_type', ''),
'search' => trim($request->query->get('q', '')),
];
$result = $service->listQuotes($filters, self::QUOTES_PER_PAGE, $offset);
$totalPages = (int)ceil($result['total'] / self::QUOTES_PER_PAGE);
// Status summary counts
$em = $this->getDoctrine()->getManager('company_group');
$conn = $em->getConnection();
$summary = [
'all' => (int)$conn->fetchOne("SELECT COUNT(*) FROM subscription_quotes"),
'requested' => (int)$conn->fetchOne("SELECT COUNT(*) FROM subscription_quotes WHERE status = 'requested'"),
'modified' => (int)$conn->fetchOne("SELECT COUNT(*) FROM subscription_quotes WHERE status = 'modified'"),
'sent' => (int)$conn->fetchOne("SELECT COUNT(*) FROM subscription_quotes WHERE status = 'sent'"),
'accepted' => (int)$conn->fetchOne("SELECT COUNT(*) FROM subscription_quotes WHERE status = 'accepted'"),
'rejected' => (int)$conn->fetchOne("SELECT COUNT(*) FROM subscription_quotes WHERE status = 'rejected'"),
'draft' => (int)$conn->fetchOne("SELECT COUNT(*) FROM subscription_quotes WHERE status = 'draft'"),
];
return $this->render('@CompanyGroup/pages/admin/quotes/list_quotes.html.twig', [
'page_title' => 'Quotes',
'quotes' => $result['items'],
'total' => $result['total'],
'currentPage' => $page,
'totalPages' => $totalPages,
'filters' => $filters,
'summary' => $summary,
]);
}
// =========================================================================
// VIEW / EDIT QUOTE
// =========================================================================
/**
* GET /admin/quotes/{id}
*/
public function ViewQuoteAction(Request $request, $id)
{
if (!$this->requireAdminAccess($request)) {
return $this->redirectToRoute('dashboard');
}
$em = $this->getDoctrine()->getManager('company_group');
$quote = $em->getRepository('CompanyGroupBundle\Entity\SubscriptionQuote')->find($id);
if (!$quote) {
throw $this->createNotFoundException('Quote #' . $id . ' not found.');
}
$service = $this->get('app.quote_service');
$pricing = $this->get('app.pricing_service');
$history = $service->getHistory((int)$quote->getId());
// Build breakdown for both Team and Enterprise (Enterprise uses same per-user rates when no flat override)
$breakdown = $pricing->getPriceBreakdown(
(int)$quote->getNormalUserCount(),
(int)$quote->getAdminUserCount(),
(int)$quote->getMlUserCount(),
$quote->getBillingCycle() ?? 'monthly',
$quote->getPlanType()
);
/** @var LegacySubscriptionBillingService $billing */
$billing = $this->get('app.legacy_subscription_billing_service');
$legacyInvoice = $billing->findQuoteInvoice($quote);
$invoice = null;
if ($legacyInvoice) {
$invoice = [
'id' => (int)$legacyInvoice->getId(),
'invoiceNumber' => (string)($legacyInvoice->getDocumentHash() ?: ('EI-' . str_pad((string)$legacyInvoice->getId(), 8, '0', STR_PAD_LEFT))),
];
}
return $this->render('@CompanyGroup/pages/admin/quotes/edit_quote.html.twig', [
'page_title' => 'Quote #' . $id,
'quote' => $quote,
'history' => $history,
'breakdown' => $breakdown,
'invoice' => $invoice,
'view_only' => true,
]);
}
/**
* GET /admin/quotes/{id}/edit — show edit form
* POST /admin/quotes/{id}/edit — save changes
*/
public function EditQuoteAction(Request $request, $id)
{
if (!$this->requireAdminAccess($request)) {
return $this->redirectToRoute('dashboard');
}
$em = $this->getDoctrine()->getManager('company_group');
$quote = $em->getRepository('CompanyGroupBundle\Entity\SubscriptionQuote')->find($id);
if (!$quote) {
throw $this->createNotFoundException('Quote #' . $id . ' not found.');
}
if ($request->isMethod('POST')) {
$post = $request->request;
$service = $this->get('app.quote_service');
$data = [
'plan_type' => $post->get('plan_type', $quote->getPlanType()),
'billing_cycle' => $post->get('billing_cycle', $quote->getBillingCycle()),
'payment_type' => $post->get('payment_type', $quote->getPaymentType()),
'normal_user_count' => max(0, (int)$post->get('normal_user_count', 0)),
'admin_user_count' => max(0, (int)$post->get('admin_user_count', 0)),
'ml_user_count' => max(0, (int)$post->get('ml_user_count', 0)),
'discount_amount' => max(0, (float)$post->get('discount_amount', 0)),
'discount_percent' => max(0, (float)$post->get('discount_percent', 0)),
'promo_code_id' => $post->get('promo_code_id', null),
'base_amount' => max(0, (float)$post->get('base_amount', 0)),
'customer_name' => trim($post->get('customer_name', '')),
'customer_phone' => trim($post->get('customer_phone', '')),
'company_name' => trim($post->get('company_name', '')),
'admin_notes' => trim($post->get('admin_notes', '')),
'company_address' => trim($post->get('company_address', '')),
'company_vat_no' => trim($post->get('company_vat_no', '')),
'customer_designation' => trim($post->get('customer_designation', '')),
'expires_at' => $post->get('expires_at', ''),
];
$service->adminModifyQuote($quote, $data, $this->loggedUserId($request));
$this->addFlash('success', 'Quote updated successfully.');
return $this->redirectToRoute('admin_quote_view', ['id' => $id]);
}
$pricing = $this->get('app.pricing_service');
$maxDiscount = null;
// Build breakdown for both Team and Enterprise
$breakdown = $pricing->getPriceBreakdown(
(int)$quote->getNormalUserCount(),
(int)$quote->getAdminUserCount(),
(int)$quote->getMlUserCount(),
$quote->getBillingCycle() ?? 'monthly',
$quote->getPlanType()
);
$adminUserId = $this->loggedUserId($request);
if ($adminUserId) {
$adminDetails = $em->getRepository('CompanyGroupBundle\Entity\EntityApplicantDetails')->find($adminUserId);
if ($adminDetails && $adminDetails->getMaxDiscountAllowed() !== null) {
$maxDiscount = (float)$adminDetails->getMaxDiscountAllowed();
}
}
return $this->render('@CompanyGroup/pages/admin/quotes/edit_quote.html.twig', [
'page_title' => 'Edit Quote #' . $id,
'quote' => $quote,
'breakdown' => $breakdown,
'view_only' => false,
'max_discount' => $maxDiscount,
]);
}
// =========================================================================
// SEND QUOTE TO CUSTOMER
// =========================================================================
/**
* POST /admin/quotes/{id}/send
*/
public function SendQuoteAction(Request $request, $id)
{
if (!$this->requireAdminAccess($request)) {
return new JsonResponse(['success' => false, 'message' => 'Unauthorized'], 403);
}
$em = $this->getDoctrine()->getManager('company_group');
$quote = $em->getRepository('CompanyGroupBundle\Entity\SubscriptionQuote')->find($id);
if (!$quote) {
return new JsonResponse(['success' => false, 'message' => 'Quote not found'], 404);
}
$service = $this->get('app.quote_service');
$service->markSent($quote, $this->loggedUserId($request));
// Quote public URL for the customer
$quoteUrl = $this->generateUrl('quote_view_customer', ['token' => $quote->getQuoteToken()], UrlGenerator::ABSOLUTE_URL);
// ── Send email notification to customer ──────────────────────────────
$customerEmail = $quote->getCustomerEmail();
$emailSent = false;
if ($customerEmail && GeneralConstant::EMAIL_ENABLED == 1) {
try {
$customerName = $quote->getCustomerName() ?: $customerEmail;
$subject = 'Your HoneyBee ERP Quote #' . $quote->getId() . ' is ready';
$body = $this->renderView('@CompanyGroup/email/quote_sent.html.twig', [
'quote' => $quote,
'quoteUrl' => $quoteUrl,
]);
$message = (new \Swift_Message($subject))
->setFrom(['contact@ourhoneybee.eu' => 'HoneyBee ERP'])
->setTo([$customerEmail => $customerName])
->setReplyTo('contact@ourhoneybee.eu')
->setBody($body, 'text/html');
$this->get('swiftmailer.mailer.quotes')->send($message);
$emailSent = true;
} catch (\Exception $e) {
// Log but don't block the admin flow — quote is already marked sent
$this->addFlash('warning', 'Quote marked as sent, but the notification email could not be delivered: ' . $e->getMessage());
}
}
if ($emailSent) {
$this->addFlash('success', 'Quote marked as sent and email delivered to ' . $customerEmail . '. Customer link: ' . $quoteUrl);
} elseif (!$emailSent && !$customerEmail) {
$this->addFlash('success', 'Quote marked as sent (no customer email on record). Customer link: ' . $quoteUrl);
}
return $this->redirectToRoute('admin_quote_view', ['id' => $id]);
}
// =========================================================================
// CREATE QUOTE (admin-initiated)
// =========================================================================
/**
* GET /admin/quotes/create — show create form
* POST /admin/quotes/create — save new quote
*/
public function CreateQuoteAction(Request $request)
{
if (!$this->requireAdminAccess($request)) {
return $this->redirectToRoute('dashboard');
}
if ($request->isMethod('POST')) {
$post = $request->request;
$service = $this->get('app.quote_service');
$data = [
'plan_type' => $post->get('plan_type', SubscriptionQuote::PLAN_TEAM),
'billing_cycle' => $post->get('billing_cycle', 'monthly'),
'payment_type' => $post->get('payment_type', 'manual'),
'normal_user_count' => max(0, (int)$post->get('normal_user_count', 0)),
'admin_user_count' => max(0, (int)$post->get('admin_user_count', 0)),
'ml_user_count' => max(0, (int)$post->get('ml_user_count', 0)),
'discount_amount' => max(0, (float)$post->get('discount_amount', 0)),
'discount_percent' => max(0, (float)$post->get('discount_percent', 0)),
'promo_code_id' => $post->get('promo_code_id', null),
'base_amount' => max(0, (float)$post->get('base_amount', 0)),
'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', '')),
'company_address' => trim($post->get('company_address', '')),
'company_vat_no' => trim($post->get('company_vat_no', '')),
'customer_designation' => trim($post->get('customer_designation', '')),
'admin_notes' => trim($post->get('admin_notes', '')),
'currency' => $post->get('currency', 'EUR'),
];
if (empty($data['customer_email'])) {
$this->addFlash('error', 'Customer email is required.');
return $this->redirectToRoute('admin_quote_create');
}
$quote = $service->createAdminQuote($data, $this->loggedUserId($request));
$this->addFlash('success', 'Quote created. Token: ' . $quote->getQuoteToken());
return $this->redirectToRoute('admin_quote_view', ['id' => $quote->getId()]);
}
$pricing = $this->get('app.pricing_service');
return $this->render('@CompanyGroup/pages/admin/quotes/create_quote.html.twig', [
'page_title' => 'Create Quote',
'price_rates' => [
'normal' => \CompanyGroupBundle\Modules\Api\Service\PricingService::PRICE_NORMAL_USER_MONTHLY,
'admin' => \CompanyGroupBundle\Modules\Api\Service\PricingService::PRICE_ADMIN_USER_MONTHLY,
'ml' => \CompanyGroupBundle\Modules\Api\Service\PricingService::PRICE_ML_USER_MONTHLY,
],
]);
}
// =========================================================================
// DUPLICATE QUOTE
// =========================================================================
// =========================================================================
// PROMO CODE LOOKUP
// =========================================================================
/**
* GET /admin/quotes/promo-lookup?code=XXX
*/
public function PromoCodeLookupAction(Request $request)
{
if (!$this->requireAdminAccess($request)) {
return new JsonResponse(['success' => false, 'message' => 'Unauthorized'], 403);
}
$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' => 'Promo code not found']);
}
$now = time();
if ($promo->getExpiresAtTs() && $promo->getExpiresAtTs() < $now) {
return new JsonResponse(['success' => false, 'message' => 'Promo code has expired']);
}
if ($promo->getStartsAtTs() && $promo->getStartsAtTs() > $now) {
return new JsonResponse(['success' => false, 'message' => 'Promo code is not yet active']);
}
if ($promo->getMaxUseCount() && $promo->getUseCountBalance() !== null && $promo->getUseCountBalance() <= 0) {
return new JsonResponse(['success' => false, 'message' => 'Promo code usage limit reached']);
}
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
? '−€' . number_format((float)$promo->getPromoValue(), 2) . ' off'
: (float)$promo->getPromoValue() . '% off',
]);
}
// =========================================================================
// DUPLICATE QUOTE
// =========================================================================
/**
* POST /admin/quotes/{id}/duplicate
*/
public function DuplicateQuoteAction(Request $request, $id)
{
if (!$this->requireAdminAccess($request)) {
return $this->redirectToRoute('dashboard');
}
$em = $this->getDoctrine()->getManager('company_group');
$quote = $em->getRepository('CompanyGroupBundle\Entity\SubscriptionQuote')->find($id);
if (!$quote) {
throw $this->createNotFoundException('Quote #' . $id . ' not found.');
}
$service = $this->get('app.quote_service');
$newQuote = $service->createAdminQuote([
'plan_type' => $quote->getPlanType(),
'billing_cycle' => $quote->getBillingCycle(),
'payment_type' => $quote->getPaymentType(),
'normal_user_count' => $quote->getNormalUserCount(),
'admin_user_count' => $quote->getAdminUserCount(),
'ml_user_count' => $quote->getMlUserCount(),
'base_amount' => $quote->getBaseAmount(),
'discount_amount' => $quote->getDiscountAmount(),
'customer_email' => $quote->getCustomerEmail(),
'customer_name' => $quote->getCustomerName(),
'customer_phone' => $quote->getCustomerPhone(),
'company_name' => $quote->getCompanyName(),
'admin_notes' => '[Duplicated from #' . $id . '] ' . $quote->getAdminNotes(),
'currency' => $quote->getCurrency() ?? 'EUR',
], $this->loggedUserId($request));
$this->addFlash('success', 'Quote duplicated as #' . $newQuote->getId());
return $this->redirectToRoute('admin_quote_edit', ['id' => $newQuote->getId()]);
}
}